update
This commit is contained in:
parent
62149477b6
commit
b2dc684ade
|
|
@ -0,0 +1,24 @@
|
|||
package entity
|
||||
|
||||
import (
|
||||
"github.com/google/uuid"
|
||||
"time"
|
||||
)
|
||||
|
||||
type ApprovalWorkflowSteps struct {
|
||||
ID uint `json:"id" gorm:"primaryKey;type:int4;autoIncrement"`
|
||||
WorkflowId uint `json:"workflow_id" gorm:"type:int4;not null"`
|
||||
StepOrder int `json:"step_order" gorm:"type:int4;not null"`
|
||||
StepName string `json:"step_name" gorm:"type:varchar;not null"`
|
||||
RequiredUserLevelId uint `json:"required_user_level_id" gorm:"type:int4;not null"`
|
||||
CanSkip *bool `json:"can_skip" gorm:"type:bool;default:false"`
|
||||
AutoApproveAfterHours *int `json:"auto_approve_after_hours" gorm:"type:int4"`
|
||||
IsActive *bool `json:"is_active" gorm:"type:bool;default:true"`
|
||||
ClientId *uuid.UUID `json:"client_id" gorm:"type:UUID"`
|
||||
CreatedAt time.Time `json:"created_at" gorm:"default:now()"`
|
||||
UpdatedAt time.Time `json:"updated_at" gorm:"default:now()"`
|
||||
|
||||
// Relations
|
||||
Workflow ApprovalWorkflows `json:"workflow" gorm:"foreignKey:WorkflowId;constraint:OnDelete:CASCADE"`
|
||||
RequiredUserLevel UserLevels `json:"required_user_level" gorm:"foreignKey:RequiredUserLevelId"`
|
||||
}
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
package entity
|
||||
|
||||
import (
|
||||
"github.com/google/uuid"
|
||||
"time"
|
||||
)
|
||||
|
||||
type ApprovalWorkflows struct {
|
||||
ID uint `json:"id" gorm:"primaryKey;type:int4;autoIncrement"`
|
||||
Name string `json:"name" gorm:"type:varchar;not null"`
|
||||
Description *string `json:"description" gorm:"type:text"`
|
||||
IsDefault *bool `json:"is_default" gorm:"type:bool;default:false"`
|
||||
IsActive *bool `json:"is_active" gorm:"type:bool;default:true"`
|
||||
// New fields for no-approval support
|
||||
RequiresApproval *bool `json:"requires_approval" gorm:"type:bool;default:true"` // false = no approval needed
|
||||
AutoPublish *bool `json:"auto_publish" gorm:"type:bool;default:false"` // true = auto publish after creation
|
||||
ClientId *uuid.UUID `json:"client_id" gorm:"type:UUID"`
|
||||
CreatedAt time.Time `json:"created_at" gorm:"default:now()"`
|
||||
UpdatedAt time.Time `json:"updated_at" gorm:"default:now()"`
|
||||
|
||||
// Relations
|
||||
Steps []ApprovalWorkflowSteps `json:"steps" gorm:"foreignKey:WorkflowId;constraint:OnDelete:CASCADE"`
|
||||
}
|
||||
|
|
@ -0,0 +1,57 @@
|
|||
package entity
|
||||
|
||||
import (
|
||||
"database/sql/driver"
|
||||
"encoding/json"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
// StringArray is a custom type for handling string arrays with JSON serialization
|
||||
type StringArray []string
|
||||
|
||||
// Scan implements the sql.Scanner interface
|
||||
func (s *StringArray) Scan(value interface{}) error {
|
||||
if value == nil {
|
||||
*s = StringArray{}
|
||||
return nil
|
||||
}
|
||||
|
||||
switch v := value.(type) {
|
||||
case []byte:
|
||||
return json.Unmarshal(v, s)
|
||||
case string:
|
||||
return json.Unmarshal([]byte(v), s)
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// Value implements the driver.Valuer interface
|
||||
func (s StringArray) Value() (driver.Value, error) {
|
||||
if s == nil || len(s) == 0 {
|
||||
return "[]", nil
|
||||
}
|
||||
return json.Marshal(s)
|
||||
}
|
||||
|
||||
type ClientApprovalSettings struct {
|
||||
ID uint `json:"id" gorm:"primaryKey;type:int4;autoIncrement"`
|
||||
ClientId uuid.UUID `json:"client_id" gorm:"type:UUID;not null;uniqueIndex"`
|
||||
RequiresApproval *bool `json:"requires_approval" gorm:"type:bool;default:true"` // false = no approval needed
|
||||
DefaultWorkflowId *uint `json:"default_workflow_id" gorm:"type:int4"` // default workflow for this client
|
||||
AutoPublishArticles *bool `json:"auto_publish_articles" gorm:"type:bool;default:false"` // auto publish after creation
|
||||
ApprovalExemptUsers []uint `json:"approval_exempt_users" gorm:"type:int4[]"` // user IDs exempt from approval
|
||||
ApprovalExemptRoles []uint `json:"approval_exempt_roles" gorm:"type:int4[]"` // role IDs exempt from approval
|
||||
ApprovalExemptCategories []uint `json:"approval_exempt_categories" gorm:"type:int4[]"` // category IDs exempt from approval
|
||||
RequireApprovalFor []string `json:"require_approval_for" gorm:"type:jsonb"` // specific content types that need approval
|
||||
SkipApprovalFor []string `json:"skip_approval_for" gorm:"type:jsonb"` // specific content types that skip approval
|
||||
IsActive *bool `json:"is_active" gorm:"type:bool;default:true"`
|
||||
CreatedAt time.Time `json:"created_at" gorm:"default:now()"`
|
||||
UpdatedAt time.Time `json:"updated_at" gorm:"default:now()"`
|
||||
|
||||
// Relations
|
||||
Client Clients `json:"client" gorm:"foreignKey:ClientId;constraint:OnDelete:CASCADE"`
|
||||
Workflow *ApprovalWorkflows `json:"workflow" gorm:"foreignKey:DefaultWorkflowId"`
|
||||
}
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
package entity
|
||||
|
||||
import (
|
||||
"github.com/google/uuid"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Clients struct {
|
||||
ID uuid.UUID `json:"id" gorm:"primaryKey;type:UUID"`
|
||||
Name string `json:"name" gorm:"type:varchar"`
|
||||
CreatedById *uint `json:"created_by_id" gorm:"type:int4"`
|
||||
IsActive *bool `json:"is_active" gorm:"type:bool;default:true"`
|
||||
CreatedAt time.Time `json:"created_at" gorm:"default:now()"`
|
||||
UpdatedAt time.Time `json:"updated_at" gorm:"default:now()"`
|
||||
}
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
package entity
|
||||
|
||||
import (
|
||||
"github.com/google/uuid"
|
||||
"time"
|
||||
)
|
||||
|
||||
type UserLevels struct {
|
||||
ID uint `json:"id" gorm:"primaryKey;type:int4;autoIncrement"`
|
||||
Name string `json:"name" gorm:"type:varchar"`
|
||||
AliasName string `json:"alias_name" gorm:"type:varchar"`
|
||||
LevelNumber int `json:"level_number" gorm:"type:int4"`
|
||||
ParentLevelId *int `json:"parent_level_id" gorm:"type:int4"`
|
||||
ProvinceId *int `json:"province_id" gorm:"type:int4"`
|
||||
Group *string `json:"group" gorm:"type:varchar"`
|
||||
IsApprovalActive *bool `json:"is_approval_active" gorm:"type:bool;default:false"`
|
||||
ClientId *uuid.UUID `json:"client_id" gorm:"type:UUID"`
|
||||
IsActive *bool `json:"is_active" gorm:"type:bool"`
|
||||
CreatedAt time.Time `json:"created_at" gorm:"default:now()"`
|
||||
UpdatedAt time.Time `json:"updated_at" gorm:"default:now()"`
|
||||
}
|
||||
|
|
@ -0,0 +1,36 @@
|
|||
package entity
|
||||
|
||||
import (
|
||||
"github.com/google/uuid"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Users struct {
|
||||
ID uint `json:"id" gorm:"primaryKey;type:int4;autoIncrement"`
|
||||
Username string `json:"username" gorm:"type:varchar"`
|
||||
Email string `json:"email" gorm:"type:varchar"`
|
||||
Fullname string `json:"fullname" gorm:"type:varchar"`
|
||||
Address *string `json:"address" gorm:"type:varchar"`
|
||||
PhoneNumber *string `json:"phone_number" gorm:"type:varchar"`
|
||||
WorkType *string `json:"work_type" gorm:"type:varchar"`
|
||||
GenderType *string `json:"gender_type" gorm:"type:varchar"`
|
||||
IdentityType *string `json:"identity_type" gorm:"type:varchar"`
|
||||
IdentityGroup *string `json:"identity_group" gorm:"type:varchar"`
|
||||
IdentityGroupNumber *string `json:"identity_group_number" gorm:"type:varchar"`
|
||||
IdentityNumber *string `json:"identity_number" gorm:"type:varchar"`
|
||||
DateOfBirth *string `json:"date_of_birth" gorm:"type:varchar"`
|
||||
LastEducation *string `json:"last_education" gorm:"type:varchar"`
|
||||
UserRoleId uint `json:"user_role_id" gorm:"type:int4"`
|
||||
UserLevelId uint `json:"user_level_id" gorm:"type:int4"`
|
||||
UserLevel *UserLevels `json:"user_levels" gorm:"foreignKey:UserLevelId;references:ID"`
|
||||
KeycloakId *string `json:"keycloak_id" gorm:"type:varchar"`
|
||||
StatusId *int `json:"status_id" gorm:"type:int4;default:1"`
|
||||
CreatedById *uint `json:"created_by_id" gorm:"type:int4"`
|
||||
ProfilePicturePath *string `json:"profile_picture_path" gorm:"type:varchar"`
|
||||
TempPassword *string `json:"temp_password" gorm:"type:varchar"`
|
||||
IsEmailUpdated *bool `json:"is_email_updated" gorm:"type:bool;default:false"`
|
||||
ClientId *uuid.UUID `json:"client_id" gorm:"type:UUID"`
|
||||
IsActive *bool `json:"is_active" gorm:"type:bool;default:true"`
|
||||
CreatedAt time.Time `json:"created_at" gorm:"default:now()"`
|
||||
UpdatedAt time.Time `json:"updated_at" gorm:"default:now()"`
|
||||
}
|
||||
|
|
@ -95,6 +95,8 @@ func Models() []interface{} {
|
|||
entity.ArticleComments{},
|
||||
entity.AuditTrails{},
|
||||
entity.Banners{},
|
||||
entity.Clients{},
|
||||
entity.ClientApprovalSettings{},
|
||||
entity.Cities{},
|
||||
entity.CsrfTokenRecords{},
|
||||
entity.CustomStaticPages{},
|
||||
|
|
|
|||
|
|
@ -0,0 +1,128 @@
|
|||
package middleware
|
||||
|
||||
import (
|
||||
"jaecoo-be/app/database/entity"
|
||||
"strings"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"github.com/google/uuid"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
const (
|
||||
ClientKeyHeader = "X-Client-Key"
|
||||
ClientContextKey = "client_id"
|
||||
)
|
||||
|
||||
// excludedPaths contains paths that don't require client key validation
|
||||
var excludedPaths = []string{
|
||||
"/swagger/*",
|
||||
"/docs/*",
|
||||
"/users/login",
|
||||
"/health/*",
|
||||
"/clients",
|
||||
"/clients/*",
|
||||
"*/viewer/*",
|
||||
"/bookmarks/test-table",
|
||||
}
|
||||
|
||||
// isPathExcluded checks if the given path should be excluded from client key validation
|
||||
func isPathExcluded(path string) bool {
|
||||
for _, excludedPath := range excludedPaths {
|
||||
if strings.HasPrefix(excludedPath, "*") && strings.HasSuffix(excludedPath, "*") {
|
||||
// Handle wildcard at both beginning and end (e.g., "*/viewer/*")
|
||||
pattern := excludedPath[1 : len(excludedPath)-1] // Remove * from both ends
|
||||
if strings.Contains(path, pattern) {
|
||||
return true
|
||||
}
|
||||
} else if strings.HasPrefix(excludedPath, "*") {
|
||||
// Handle wildcard at the beginning
|
||||
if strings.HasSuffix(path, excludedPath[1:]) {
|
||||
return true
|
||||
}
|
||||
} else if strings.HasSuffix(excludedPath, "*") {
|
||||
// Handle wildcard at the end
|
||||
prefix := excludedPath[:len(excludedPath)-1]
|
||||
if strings.HasPrefix(path, prefix) {
|
||||
return true
|
||||
}
|
||||
} else {
|
||||
// Exact match
|
||||
if path == excludedPath {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// ClientMiddleware extracts and validates the Client Key from request headers
|
||||
func ClientMiddleware(db *gorm.DB) fiber.Handler {
|
||||
return func(c *fiber.Ctx) error {
|
||||
// Check if path should be excluded from client key validation
|
||||
if isPathExcluded(c.Path()) {
|
||||
return c.Next()
|
||||
}
|
||||
|
||||
// Extract Client Key from header
|
||||
clientKey := c.Get(ClientKeyHeader)
|
||||
|
||||
if clientKey == "" {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
|
||||
"success": false,
|
||||
"code": 400,
|
||||
"messages": []string{"Client Key is required in header: " + ClientKeyHeader},
|
||||
})
|
||||
}
|
||||
|
||||
// Parse UUID
|
||||
clientUUID, err := uuid.Parse(clientKey)
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
|
||||
"success": false,
|
||||
"code": 400,
|
||||
"messages": []string{"Invalid Client Key format"},
|
||||
})
|
||||
}
|
||||
|
||||
// Validate client exists and is active
|
||||
var client entity.Clients
|
||||
if err := db.Where("id = ? AND is_active = ?", clientUUID, true).First(&client).Error; err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{
|
||||
"success": false,
|
||||
"code": 401,
|
||||
"messages": []string{"Invalid or inactive Client Key"},
|
||||
})
|
||||
}
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
|
||||
"success": false,
|
||||
"code": 500,
|
||||
"messages": []string{"Error validating Client Key"},
|
||||
})
|
||||
}
|
||||
|
||||
// Store client ID in context for use in handlers
|
||||
c.Locals(ClientContextKey, clientUUID)
|
||||
|
||||
return c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
// GetClientID retrieves the client ID from the context
|
||||
func GetClientID(c *fiber.Ctx) *uuid.UUID {
|
||||
if clientID, ok := c.Locals(ClientContextKey).(uuid.UUID); ok {
|
||||
return &clientID
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// AddExcludedPath adds a new path to the excluded paths list
|
||||
func AddExcludedPath(path string) {
|
||||
excludedPaths = append(excludedPaths, path)
|
||||
}
|
||||
|
||||
// GetExcludedPaths returns the current list of excluded paths
|
||||
func GetExcludedPaths() []string {
|
||||
return excludedPaths
|
||||
}
|
||||
|
|
@ -0,0 +1,57 @@
|
|||
package middleware
|
||||
|
||||
import (
|
||||
"jaecoo-be/app/database/entity/users"
|
||||
"jaecoo-be/app/module/users/repository"
|
||||
utilSvc "jaecoo-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
|
||||
}
|
||||
|
|
@ -0,0 +1,65 @@
|
|||
package client_approval_settings
|
||||
|
||||
import (
|
||||
"jaecoo-be/app/module/client_approval_settings/controller"
|
||||
"jaecoo-be/app/module/client_approval_settings/repository"
|
||||
"jaecoo-be/app/module/client_approval_settings/service"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"go.uber.org/fx"
|
||||
)
|
||||
|
||||
// ClientApprovalSettingsRouter struct of ClientApprovalSettingsRouter
|
||||
type ClientApprovalSettingsRouter struct {
|
||||
App fiber.Router
|
||||
Controller *controller.Controller
|
||||
}
|
||||
|
||||
// NewClientApprovalSettingsModule register bulky of ClientApprovalSettings module
|
||||
var NewClientApprovalSettingsModule = fx.Options(
|
||||
// register repository of ClientApprovalSettings module
|
||||
fx.Provide(repository.NewClientApprovalSettingsRepository),
|
||||
|
||||
// register service of ClientApprovalSettings module
|
||||
fx.Provide(service.NewClientApprovalSettingsService),
|
||||
|
||||
// register controller of ClientApprovalSettings module
|
||||
fx.Provide(controller.NewController),
|
||||
|
||||
// register router of ClientApprovalSettings module
|
||||
fx.Provide(NewClientApprovalSettingsRouter),
|
||||
)
|
||||
|
||||
// NewClientApprovalSettingsRouter init ClientApprovalSettingsRouter
|
||||
func NewClientApprovalSettingsRouter(fiber *fiber.App, controller *controller.Controller) *ClientApprovalSettingsRouter {
|
||||
return &ClientApprovalSettingsRouter{
|
||||
App: fiber,
|
||||
Controller: controller,
|
||||
}
|
||||
}
|
||||
|
||||
// RegisterClientApprovalSettingsRoutes register routes of ClientApprovalSettings
|
||||
func (_i *ClientApprovalSettingsRouter) RegisterClientApprovalSettingsRoutes() {
|
||||
// define controllers
|
||||
clientApprovalSettingsController := _i.Controller.ClientApprovalSettings
|
||||
|
||||
// define routes
|
||||
_i.App.Route("/client-approval-settings", func(router fiber.Router) {
|
||||
// Basic CRUD routes
|
||||
router.Post("/", clientApprovalSettingsController.CreateSettings)
|
||||
router.Get("/", clientApprovalSettingsController.GetSettings)
|
||||
router.Put("/", clientApprovalSettingsController.UpdateSettings)
|
||||
router.Delete("/", clientApprovalSettingsController.DeleteSettings)
|
||||
|
||||
// Approval management routes
|
||||
router.Post("/toggle-approval", clientApprovalSettingsController.ToggleApproval)
|
||||
router.Post("/enable-approval", clientApprovalSettingsController.EnableApproval)
|
||||
router.Post("/disable-approval", clientApprovalSettingsController.DisableApproval)
|
||||
router.Put("/default-workflow", clientApprovalSettingsController.SetDefaultWorkflow)
|
||||
|
||||
// Exemption management routes
|
||||
router.Post("/exempt-users", clientApprovalSettingsController.ManageExemptUsers)
|
||||
router.Post("/exempt-roles", clientApprovalSettingsController.ManageExemptRoles)
|
||||
router.Post("/exempt-categories", clientApprovalSettingsController.ManageExemptCategories)
|
||||
})
|
||||
}
|
||||
|
|
@ -0,0 +1,430 @@
|
|||
package controller
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"jaecoo-be/app/middleware"
|
||||
"jaecoo-be/app/module/client_approval_settings/request"
|
||||
"jaecoo-be/app/module/client_approval_settings/service"
|
||||
utilRes "jaecoo-be/utils/response"
|
||||
utilVal "jaecoo-be/utils/validator"
|
||||
"strconv"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"github.com/rs/zerolog"
|
||||
)
|
||||
|
||||
type clientApprovalSettingsController struct {
|
||||
clientApprovalSettingsService service.ClientApprovalSettingsService
|
||||
Log zerolog.Logger
|
||||
}
|
||||
|
||||
type ClientApprovalSettingsController interface {
|
||||
CreateSettings(c *fiber.Ctx) error
|
||||
GetSettings(c *fiber.Ctx) error
|
||||
UpdateSettings(c *fiber.Ctx) error
|
||||
DeleteSettings(c *fiber.Ctx) error
|
||||
ToggleApproval(c *fiber.Ctx) error
|
||||
EnableApproval(c *fiber.Ctx) error
|
||||
DisableApproval(c *fiber.Ctx) error
|
||||
SetDefaultWorkflow(c *fiber.Ctx) error
|
||||
ManageExemptUsers(c *fiber.Ctx) error
|
||||
ManageExemptRoles(c *fiber.Ctx) error
|
||||
ManageExemptCategories(c *fiber.Ctx) error
|
||||
}
|
||||
|
||||
func NewClientApprovalSettingsController(
|
||||
clientApprovalSettingsService service.ClientApprovalSettingsService,
|
||||
log zerolog.Logger,
|
||||
) ClientApprovalSettingsController {
|
||||
return &clientApprovalSettingsController{
|
||||
clientApprovalSettingsService: clientApprovalSettingsService,
|
||||
Log: log,
|
||||
}
|
||||
}
|
||||
|
||||
// CreateSettings ClientApprovalSettings
|
||||
// @Summary Create Client Approval Settings
|
||||
// @Description API for creating client approval settings
|
||||
// @Tags ClientApprovalSettings
|
||||
// @Security Bearer
|
||||
// @Param X-Client-Key header string true "Insert the X-Client-Key"
|
||||
// @Param payload body request.CreateClientApprovalSettingsRequest true "Required payload"
|
||||
// @Success 200 {object} response.Response
|
||||
// @Failure 400 {object} response.BadRequestError
|
||||
// @Failure 401 {object} response.UnauthorizedError
|
||||
// @Failure 500 {object} response.InternalServerError
|
||||
// @Router /client-approval-settings [post]
|
||||
func (_i *clientApprovalSettingsController) CreateSettings(c *fiber.Ctx) error {
|
||||
req := new(request.CreateClientApprovalSettingsRequest)
|
||||
if err := utilVal.ParseAndValidate(c, req); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Get ClientId from context
|
||||
clientId := middleware.GetClientID(c)
|
||||
|
||||
settings, err := _i.clientApprovalSettingsService.Create(clientId, *req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return utilRes.Resp(c, utilRes.Response{
|
||||
Success: true,
|
||||
Messages: utilRes.Messages{"Client approval settings created successfully"},
|
||||
Data: settings,
|
||||
})
|
||||
}
|
||||
|
||||
// GetSettings ClientApprovalSettings
|
||||
// @Summary Get Client Approval Settings
|
||||
// @Description API for getting client approval settings
|
||||
// @Tags ClientApprovalSettings
|
||||
// @Security Bearer
|
||||
// @Param X-Client-Key header string true "Insert the X-Client-Key"
|
||||
// @Success 200 {object} response.Response
|
||||
// @Failure 400 {object} response.BadRequestError
|
||||
// @Failure 401 {object} response.UnauthorizedError
|
||||
// @Failure 500 {object} response.InternalServerError
|
||||
// @Router /client-approval-settings [get]
|
||||
func (_i *clientApprovalSettingsController) GetSettings(c *fiber.Ctx) error {
|
||||
// Get ClientId from context
|
||||
clientId := middleware.GetClientID(c)
|
||||
|
||||
settings, err := _i.clientApprovalSettingsService.GetByClientId(clientId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return utilRes.Resp(c, utilRes.Response{
|
||||
Success: true,
|
||||
Messages: utilRes.Messages{"Client approval settings successfully retrieved"},
|
||||
Data: settings,
|
||||
})
|
||||
}
|
||||
|
||||
// UpdateSettings ClientApprovalSettings
|
||||
// @Summary Update Client Approval Settings
|
||||
// @Description API for updating client approval settings
|
||||
// @Tags ClientApprovalSettings
|
||||
// @Security Bearer
|
||||
// @Param X-Client-Key header string true "Insert the X-Client-Key"
|
||||
// @Param payload body request.UpdateClientApprovalSettingsRequest 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 [put]
|
||||
func (_i *clientApprovalSettingsController) UpdateSettings(c *fiber.Ctx) error {
|
||||
req := new(request.UpdateClientApprovalSettingsRequest)
|
||||
if err := utilVal.ParseAndValidate(c, req); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Get ClientId from context
|
||||
clientId := middleware.GetClientID(c)
|
||||
|
||||
settings, err := _i.clientApprovalSettingsService.Update(clientId, *req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return utilRes.Resp(c, utilRes.Response{
|
||||
Success: true,
|
||||
Messages: utilRes.Messages{"Client approval settings successfully updated"},
|
||||
Data: settings,
|
||||
})
|
||||
}
|
||||
|
||||
// DeleteSettings ClientApprovalSettings
|
||||
// @Summary Delete Client Approval Settings
|
||||
// @Description API for deleting client approval settings
|
||||
// @Tags ClientApprovalSettings
|
||||
// @Security Bearer
|
||||
// @Param X-Client-Key header string true "Insert the X-Client-Key"
|
||||
// @Success 200 {object} response.Response
|
||||
// @Failure 400 {object} response.BadRequestError
|
||||
// @Failure 401 {object} response.UnauthorizedError
|
||||
// @Failure 500 {object} response.InternalServerError
|
||||
// @Router /client-approval-settings [delete]
|
||||
func (_i *clientApprovalSettingsController) DeleteSettings(c *fiber.Ctx) error {
|
||||
// Get ClientId from context
|
||||
clientId := middleware.GetClientID(c)
|
||||
|
||||
err := _i.clientApprovalSettingsService.Delete(clientId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return utilRes.Resp(c, utilRes.Response{
|
||||
Success: true,
|
||||
Messages: utilRes.Messages{"Client approval settings successfully deleted"},
|
||||
})
|
||||
}
|
||||
|
||||
// ToggleApproval ClientApprovalSettings
|
||||
// @Summary Toggle Approval Requirement
|
||||
// @Description API for toggling approval requirement on/off
|
||||
// @Tags ClientApprovalSettings
|
||||
// @Security Bearer
|
||||
// @Param X-Client-Key header string true "Insert the X-Client-Key"
|
||||
// @Param payload body request.ToggleApprovalRequest 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/toggle [post]
|
||||
func (_i *clientApprovalSettingsController) ToggleApproval(c *fiber.Ctx) error {
|
||||
req := new(request.ToggleApprovalRequest)
|
||||
if err := utilVal.ParseAndValidate(c, req); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Get ClientId from context
|
||||
clientId := middleware.GetClientID(c)
|
||||
|
||||
err := _i.clientApprovalSettingsService.ToggleApprovalRequirement(clientId, req.RequiresApproval)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
action := "enabled"
|
||||
if !req.RequiresApproval {
|
||||
action = "disabled with auto-publish"
|
||||
}
|
||||
|
||||
return utilRes.Resp(c, utilRes.Response{
|
||||
Success: true,
|
||||
Messages: utilRes.Messages{fmt.Sprintf("Approval system successfully %s", action)},
|
||||
})
|
||||
}
|
||||
|
||||
// EnableApproval ClientApprovalSettings
|
||||
// @Summary Enable Approval System
|
||||
// @Description API for enabling approval system with smooth transition
|
||||
// @Tags ClientApprovalSettings
|
||||
// @Security Bearer
|
||||
// @Param X-Client-Key header string true "Insert the X-Client-Key"
|
||||
// @Param payload body request.EnableApprovalRequest 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/enable [post]
|
||||
func (_i *clientApprovalSettingsController) EnableApproval(c *fiber.Ctx) error {
|
||||
req := new(request.EnableApprovalRequest)
|
||||
if err := utilVal.ParseAndValidate(c, req); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Get ClientId from context
|
||||
clientId := middleware.GetClientID(c)
|
||||
|
||||
err := _i.clientApprovalSettingsService.EnableApprovalWithTransition(clientId, req.DefaultWorkflowId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return utilRes.Resp(c, utilRes.Response{
|
||||
Success: true,
|
||||
Messages: utilRes.Messages{"Approval system successfully enabled with smooth transition"},
|
||||
})
|
||||
}
|
||||
|
||||
// DisableApproval ClientApprovalSettings
|
||||
// @Summary Disable Approval System
|
||||
// @Description API for disabling approval system and auto-publish pending articles
|
||||
// @Tags ClientApprovalSettings
|
||||
// @Security Bearer
|
||||
// @Param X-Client-Key header string true "Insert the X-Client-Key"
|
||||
// @Param payload body request.DisableApprovalRequest 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/disable [post]
|
||||
func (_i *clientApprovalSettingsController) DisableApproval(c *fiber.Ctx) error {
|
||||
req := new(request.DisableApprovalRequest)
|
||||
if err := utilVal.ParseAndValidate(c, req); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Get ClientId from context
|
||||
clientId := middleware.GetClientID(c)
|
||||
|
||||
err := _i.clientApprovalSettingsService.DisableApprovalWithAutoPublish(clientId, req.Reason)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return utilRes.Resp(c, utilRes.Response{
|
||||
Success: true,
|
||||
Messages: utilRes.Messages{"Approval system successfully disabled with auto-publish enabled"},
|
||||
})
|
||||
}
|
||||
|
||||
// SetDefaultWorkflow ClientApprovalSettings
|
||||
// @Summary Set Default Workflow
|
||||
// @Description API for setting default workflow for client
|
||||
// @Tags ClientApprovalSettings
|
||||
// @Security Bearer
|
||||
// @Param X-Client-Key header string true "Insert the X-Client-Key"
|
||||
// @Param payload body request.SetDefaultWorkflowRequest 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/default-workflow [post]
|
||||
func (_i *clientApprovalSettingsController) SetDefaultWorkflow(c *fiber.Ctx) error {
|
||||
req := new(request.SetDefaultWorkflowRequest)
|
||||
if err := utilVal.ParseAndValidate(c, req); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Get ClientId from context
|
||||
clientId := middleware.GetClientID(c)
|
||||
|
||||
err := _i.clientApprovalSettingsService.SetDefaultWorkflow(clientId, req.WorkflowId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return utilRes.Resp(c, utilRes.Response{
|
||||
Success: true,
|
||||
Messages: utilRes.Messages{"Default workflow successfully set"},
|
||||
})
|
||||
}
|
||||
|
||||
// ManageExemptUsers ClientApprovalSettings
|
||||
// @Summary Manage Exempt Users
|
||||
// @Description API for adding/removing users from approval exemption
|
||||
// @Tags ClientApprovalSettings
|
||||
// @Security Bearer
|
||||
// @Param X-Client-Key header string true "Insert the X-Client-Key"
|
||||
// @Param action path string true "Action: add or remove"
|
||||
// @Param user_id path int true "User ID"
|
||||
// @Success 200 {object} response.Response
|
||||
// @Failure 400 {object} response.BadRequestError
|
||||
// @Failure 401 {object} response.UnauthorizedError
|
||||
// @Failure 500 {object} response.InternalServerError
|
||||
// @Router /client-approval-settings/exempt-users/{action}/{user_id} [post]
|
||||
func (_i *clientApprovalSettingsController) ManageExemptUsers(c *fiber.Ctx) error {
|
||||
action := c.Params("action")
|
||||
userIdStr := c.Params("user_id")
|
||||
|
||||
if action != "add" && action != "remove" {
|
||||
return utilRes.ErrorBadRequest(c, "Invalid action. Use 'add' or 'remove'")
|
||||
}
|
||||
|
||||
userId, err := strconv.Atoi(userIdStr)
|
||||
if err != nil {
|
||||
return utilRes.ErrorBadRequest(c, "Invalid user ID format")
|
||||
}
|
||||
|
||||
// Get ClientId from context
|
||||
clientId := middleware.GetClientID(c)
|
||||
|
||||
if action == "add" {
|
||||
err = _i.clientApprovalSettingsService.AddExemptUser(clientId, uint(userId))
|
||||
} else {
|
||||
err = _i.clientApprovalSettingsService.RemoveExemptUser(clientId, uint(userId))
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return utilRes.Resp(c, utilRes.Response{
|
||||
Success: true,
|
||||
Messages: utilRes.Messages{fmt.Sprintf("User successfully %sd from approval exemption", action)},
|
||||
})
|
||||
}
|
||||
|
||||
// ManageExemptRoles ClientApprovalSettings
|
||||
// @Summary Manage Exempt Roles
|
||||
// @Description API for adding/removing roles from approval exemption
|
||||
// @Tags ClientApprovalSettings
|
||||
// @Security Bearer
|
||||
// @Param X-Client-Key header string true "Insert the X-Client-Key"
|
||||
// @Param action path string true "Action: add or remove"
|
||||
// @Param role_id path int true "Role ID"
|
||||
// @Success 200 {object} response.Response
|
||||
// @Failure 400 {object} response.BadRequestError
|
||||
// @Failure 401 {object} response.UnauthorizedError
|
||||
// @Failure 500 {object} response.InternalServerError
|
||||
// @Router /client-approval-settings/exempt-roles/{action}/{role_id} [post]
|
||||
func (_i *clientApprovalSettingsController) ManageExemptRoles(c *fiber.Ctx) error {
|
||||
action := c.Params("action")
|
||||
roleIdStr := c.Params("role_id")
|
||||
|
||||
if action != "add" && action != "remove" {
|
||||
return utilRes.ErrorBadRequest(c, "Invalid action. Use 'add' or 'remove'")
|
||||
}
|
||||
|
||||
roleId, err := strconv.Atoi(roleIdStr)
|
||||
if err != nil {
|
||||
return utilRes.ErrorBadRequest(c, "Invalid role ID format")
|
||||
}
|
||||
|
||||
// Get ClientId from context
|
||||
clientId := middleware.GetClientID(c)
|
||||
|
||||
if action == "add" {
|
||||
err = _i.clientApprovalSettingsService.AddExemptRole(clientId, uint(roleId))
|
||||
} else {
|
||||
err = _i.clientApprovalSettingsService.RemoveExemptRole(clientId, uint(roleId))
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return utilRes.Resp(c, utilRes.Response{
|
||||
Success: true,
|
||||
Messages: utilRes.Messages{fmt.Sprintf("Role successfully %sd from approval exemption", action)},
|
||||
})
|
||||
}
|
||||
|
||||
// ManageExemptCategories ClientApprovalSettings
|
||||
// @Summary Manage Exempt Categories
|
||||
// @Description API for adding/removing categories from approval exemption
|
||||
// @Tags ClientApprovalSettings
|
||||
// @Security Bearer
|
||||
// @Param X-Client-Key header string true "Insert the X-Client-Key"
|
||||
// @Param action path string true "Action: add or remove"
|
||||
// @Param category_id path int true "Category ID"
|
||||
// @Success 200 {object} response.Response
|
||||
// @Failure 400 {object} response.BadRequestError
|
||||
// @Failure 401 {object} response.UnauthorizedError
|
||||
// @Failure 500 {object} response.InternalServerError
|
||||
// @Router /client-approval-settings/exempt-categories/{action}/{category_id} [post]
|
||||
func (_i *clientApprovalSettingsController) ManageExemptCategories(c *fiber.Ctx) error {
|
||||
action := c.Params("action")
|
||||
categoryIdStr := c.Params("category_id")
|
||||
|
||||
if action != "add" && action != "remove" {
|
||||
return utilRes.ErrorBadRequest(c, "Invalid action. Use 'add' or 'remove'")
|
||||
}
|
||||
|
||||
categoryId, err := strconv.Atoi(categoryIdStr)
|
||||
if err != nil {
|
||||
return utilRes.ErrorBadRequest(c, "Invalid category ID format")
|
||||
}
|
||||
|
||||
// Get ClientId from context
|
||||
clientId := middleware.GetClientID(c)
|
||||
|
||||
if action == "add" {
|
||||
err = _i.clientApprovalSettingsService.AddExemptCategory(clientId, uint(categoryId))
|
||||
} else {
|
||||
err = _i.clientApprovalSettingsService.RemoveExemptCategory(clientId, uint(categoryId))
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return utilRes.Resp(c, utilRes.Response{
|
||||
Success: true,
|
||||
Messages: utilRes.Messages{fmt.Sprintf("Category successfully %sd from approval exemption", action)},
|
||||
})
|
||||
}
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
package controller
|
||||
|
||||
import (
|
||||
"jaecoo-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),
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,104 @@
|
|||
package mapper
|
||||
|
||||
import (
|
||||
"jaecoo-be/app/database/entity"
|
||||
res "jaecoo-be/app/module/client_approval_settings/response"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/rs/zerolog"
|
||||
)
|
||||
|
||||
func ClientApprovalSettingsResponseMapper(
|
||||
log zerolog.Logger,
|
||||
clientId *uuid.UUID,
|
||||
settings *entity.ClientApprovalSettings,
|
||||
) *res.ClientApprovalSettingsResponse {
|
||||
if settings == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return &res.ClientApprovalSettingsResponse{
|
||||
ID: settings.ID,
|
||||
ClientId: settings.ClientId.String(),
|
||||
RequiresApproval: *settings.RequiresApproval,
|
||||
DefaultWorkflowId: settings.DefaultWorkflowId,
|
||||
AutoPublishArticles: *settings.AutoPublishArticles,
|
||||
ApprovalExemptUsers: settings.ApprovalExemptUsers,
|
||||
ApprovalExemptRoles: settings.ApprovalExemptRoles,
|
||||
ApprovalExemptCategories: settings.ApprovalExemptCategories,
|
||||
RequireApprovalFor: settings.RequireApprovalFor,
|
||||
SkipApprovalFor: settings.SkipApprovalFor,
|
||||
IsActive: *settings.IsActive,
|
||||
CreatedAt: settings.CreatedAt,
|
||||
UpdatedAt: settings.UpdatedAt,
|
||||
}
|
||||
}
|
||||
|
||||
func ClientApprovalSettingsDetailResponseMapper(
|
||||
log zerolog.Logger,
|
||||
clientId *uuid.UUID,
|
||||
settings *entity.ClientApprovalSettings,
|
||||
) *res.ClientApprovalSettingsDetailResponse {
|
||||
if settings == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
response := &res.ClientApprovalSettingsDetailResponse{
|
||||
ID: settings.ID,
|
||||
ClientId: settings.ClientId.String(),
|
||||
RequiresApproval: *settings.RequiresApproval,
|
||||
DefaultWorkflowId: settings.DefaultWorkflowId,
|
||||
AutoPublishArticles: *settings.AutoPublishArticles,
|
||||
ApprovalExemptUsers: settings.ApprovalExemptUsers,
|
||||
ApprovalExemptRoles: settings.ApprovalExemptRoles,
|
||||
ApprovalExemptCategories: settings.ApprovalExemptCategories,
|
||||
RequireApprovalFor: settings.RequireApprovalFor,
|
||||
SkipApprovalFor: settings.SkipApprovalFor,
|
||||
IsActive: *settings.IsActive,
|
||||
CreatedAt: settings.CreatedAt,
|
||||
UpdatedAt: settings.UpdatedAt,
|
||||
}
|
||||
|
||||
// Add client relation if available
|
||||
if settings.Client.ID != uuid.Nil {
|
||||
response.Client = &res.ClientResponse{
|
||||
ID: 1, // Placeholder - would need proper ID mapping
|
||||
Name: settings.Client.Name,
|
||||
IsActive: *settings.Client.IsActive,
|
||||
}
|
||||
}
|
||||
|
||||
// Add workflow relation if available
|
||||
if settings.Workflow != nil && settings.Workflow.ID != 0 {
|
||||
response.Workflow = &res.WorkflowResponse{
|
||||
ID: settings.Workflow.ID,
|
||||
Name: settings.Workflow.Name,
|
||||
Description: settings.Workflow.Description,
|
||||
IsActive: *settings.Workflow.IsActive,
|
||||
}
|
||||
}
|
||||
|
||||
return response
|
||||
}
|
||||
|
||||
func ApprovalStatusResponseMapper(
|
||||
log zerolog.Logger,
|
||||
clientId *uuid.UUID,
|
||||
settings *entity.ClientApprovalSettings,
|
||||
) *res.ApprovalStatusResponse {
|
||||
if settings == nil {
|
||||
return &res.ApprovalStatusResponse{
|
||||
RequiresApproval: true, // Default to requiring approval
|
||||
AutoPublishArticles: false,
|
||||
IsActive: false,
|
||||
Message: "No approval settings found",
|
||||
}
|
||||
}
|
||||
|
||||
return &res.ApprovalStatusResponse{
|
||||
RequiresApproval: *settings.RequiresApproval,
|
||||
AutoPublishArticles: *settings.AutoPublishArticles,
|
||||
IsActive: *settings.IsActive,
|
||||
Message: "Approval settings loaded successfully",
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
package repository
|
||||
|
||||
import (
|
||||
"jaecoo-be/app/database/entity"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type ClientApprovalSettingsRepository interface {
|
||||
// Basic CRUD
|
||||
Create(clientId *uuid.UUID, settings *entity.ClientApprovalSettings) (*entity.ClientApprovalSettings, error)
|
||||
FindOne(clientId *uuid.UUID) (*entity.ClientApprovalSettings, error)
|
||||
Update(clientId *uuid.UUID, settings *entity.ClientApprovalSettings) (*entity.ClientApprovalSettings, error)
|
||||
Delete(clientId *uuid.UUID) error
|
||||
|
||||
// Specific queries
|
||||
FindByClientId(clientId uuid.UUID) (*entity.ClientApprovalSettings, error)
|
||||
FindActiveSettings(clientId *uuid.UUID) (*entity.ClientApprovalSettings, error)
|
||||
FindByWorkflowId(workflowId uint) ([]*entity.ClientApprovalSettings, error)
|
||||
|
||||
// Exemption management
|
||||
AddExemptUser(clientId *uuid.UUID, userId uint) error
|
||||
RemoveExemptUser(clientId *uuid.UUID, userId uint) error
|
||||
AddExemptRole(clientId *uuid.UUID, roleId uint) error
|
||||
RemoveExemptRole(clientId *uuid.UUID, roleId uint) error
|
||||
AddExemptCategory(clientId *uuid.UUID, categoryId uint) error
|
||||
RemoveExemptCategory(clientId *uuid.UUID, categoryId uint) error
|
||||
|
||||
// Bulk operations
|
||||
BulkUpdateExemptUsers(clientId *uuid.UUID, userIds []uint) error
|
||||
BulkUpdateExemptRoles(clientId *uuid.UUID, roleIds []uint) error
|
||||
BulkUpdateExemptCategories(clientId *uuid.UUID, categoryIds []uint) error
|
||||
}
|
||||
|
|
@ -0,0 +1,139 @@
|
|||
package repository
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"jaecoo-be/app/database"
|
||||
"jaecoo-be/app/database/entity"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/rs/zerolog"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type clientApprovalSettingsRepository struct {
|
||||
DB *database.Database
|
||||
Log zerolog.Logger
|
||||
}
|
||||
|
||||
func NewClientApprovalSettingsRepository(db *database.Database, log zerolog.Logger) ClientApprovalSettingsRepository {
|
||||
return &clientApprovalSettingsRepository{
|
||||
DB: db,
|
||||
Log: log,
|
||||
}
|
||||
}
|
||||
|
||||
func (r *clientApprovalSettingsRepository) Create(clientId *uuid.UUID, settings *entity.ClientApprovalSettings) (*entity.ClientApprovalSettings, error) {
|
||||
settings.ClientId = *clientId
|
||||
if err := r.DB.DB.Create(settings).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return settings, nil
|
||||
}
|
||||
|
||||
func (r *clientApprovalSettingsRepository) FindOne(clientId *uuid.UUID) (*entity.ClientApprovalSettings, error) {
|
||||
var settings entity.ClientApprovalSettings
|
||||
err := r.DB.DB.Where("client_id = ?", clientId).First(&settings).Error
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
return &settings, nil
|
||||
}
|
||||
|
||||
func (r *clientApprovalSettingsRepository) Update(clientId *uuid.UUID, settings *entity.ClientApprovalSettings) (*entity.ClientApprovalSettings, error) {
|
||||
settings.ClientId = *clientId
|
||||
if err := r.DB.DB.Where("client_id = ?", clientId).Save(settings).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return settings, nil
|
||||
}
|
||||
|
||||
func (r *clientApprovalSettingsRepository) Delete(clientId *uuid.UUID) error {
|
||||
return r.DB.DB.Where("client_id = ?", clientId).Delete(&entity.ClientApprovalSettings{}).Error
|
||||
}
|
||||
|
||||
func (r *clientApprovalSettingsRepository) FindByClientId(clientId uuid.UUID) (*entity.ClientApprovalSettings, error) {
|
||||
var settings entity.ClientApprovalSettings
|
||||
err := r.DB.DB.Where("client_id = ?", clientId).First(&settings).Error
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
return &settings, nil
|
||||
}
|
||||
|
||||
func (r *clientApprovalSettingsRepository) FindActiveSettings(clientId *uuid.UUID) (*entity.ClientApprovalSettings, error) {
|
||||
var settings entity.ClientApprovalSettings
|
||||
err := r.DB.DB.Where("client_id = ? AND is_active = ?", clientId, true).First(&settings).Error
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
return &settings, nil
|
||||
}
|
||||
|
||||
func (r *clientApprovalSettingsRepository) FindByWorkflowId(workflowId uint) ([]*entity.ClientApprovalSettings, error) {
|
||||
var settings []*entity.ClientApprovalSettings
|
||||
err := r.DB.DB.Where("default_workflow_id = ?", workflowId).Find(&settings).Error
|
||||
return settings, err
|
||||
}
|
||||
|
||||
func (r *clientApprovalSettingsRepository) AddExemptUser(clientId *uuid.UUID, userId uint) error {
|
||||
return r.DB.DB.Model(&entity.ClientApprovalSettings{}).
|
||||
Where("client_id = ?", clientId).
|
||||
Update("approval_exempt_users", gorm.Expr("array_append(approval_exempt_users, ?)", userId)).Error
|
||||
}
|
||||
|
||||
func (r *clientApprovalSettingsRepository) RemoveExemptUser(clientId *uuid.UUID, userId uint) error {
|
||||
return r.DB.DB.Model(&entity.ClientApprovalSettings{}).
|
||||
Where("client_id = ?", clientId).
|
||||
Update("approval_exempt_users", gorm.Expr("array_remove(approval_exempt_users, ?)", userId)).Error
|
||||
}
|
||||
|
||||
func (r *clientApprovalSettingsRepository) AddExemptRole(clientId *uuid.UUID, roleId uint) error {
|
||||
return r.DB.DB.Model(&entity.ClientApprovalSettings{}).
|
||||
Where("client_id = ?", clientId).
|
||||
Update("approval_exempt_roles", gorm.Expr("array_append(approval_exempt_roles, ?)", roleId)).Error
|
||||
}
|
||||
|
||||
func (r *clientApprovalSettingsRepository) RemoveExemptRole(clientId *uuid.UUID, roleId uint) error {
|
||||
return r.DB.DB.Model(&entity.ClientApprovalSettings{}).
|
||||
Where("client_id = ?", clientId).
|
||||
Update("approval_exempt_roles", gorm.Expr("array_remove(approval_exempt_roles, ?)", roleId)).Error
|
||||
}
|
||||
|
||||
func (r *clientApprovalSettingsRepository) AddExemptCategory(clientId *uuid.UUID, categoryId uint) error {
|
||||
return r.DB.DB.Model(&entity.ClientApprovalSettings{}).
|
||||
Where("client_id = ?", clientId).
|
||||
Update("approval_exempt_categories", gorm.Expr("array_append(approval_exempt_categories, ?)", categoryId)).Error
|
||||
}
|
||||
|
||||
func (r *clientApprovalSettingsRepository) RemoveExemptCategory(clientId *uuid.UUID, categoryId uint) error {
|
||||
return r.DB.DB.Model(&entity.ClientApprovalSettings{}).
|
||||
Where("client_id = ?", clientId).
|
||||
Update("approval_exempt_categories", gorm.Expr("array_remove(approval_exempt_categories, ?)", categoryId)).Error
|
||||
}
|
||||
|
||||
func (r *clientApprovalSettingsRepository) BulkUpdateExemptUsers(clientId *uuid.UUID, userIds []uint) error {
|
||||
return r.DB.DB.Model(&entity.ClientApprovalSettings{}).
|
||||
Where("client_id = ?", clientId).
|
||||
Update("approval_exempt_users", userIds).Error
|
||||
}
|
||||
|
||||
func (r *clientApprovalSettingsRepository) BulkUpdateExemptRoles(clientId *uuid.UUID, roleIds []uint) error {
|
||||
return r.DB.DB.Model(&entity.ClientApprovalSettings{}).
|
||||
Where("client_id = ?", clientId).
|
||||
Update("approval_exempt_roles", roleIds).Error
|
||||
}
|
||||
|
||||
func (r *clientApprovalSettingsRepository) BulkUpdateExemptCategories(clientId *uuid.UUID, categoryIds []uint) error {
|
||||
return r.DB.DB.Model(&entity.ClientApprovalSettings{}).
|
||||
Where("client_id = ?", clientId).
|
||||
Update("approval_exempt_categories", categoryIds).Error
|
||||
}
|
||||
|
|
@ -0,0 +1,85 @@
|
|||
package request
|
||||
|
||||
// CreateClientApprovalSettingsRequest represents request for creating client approval settings
|
||||
type CreateClientApprovalSettingsRequest struct {
|
||||
RequiresApproval bool `json:"requiresApproval"`
|
||||
DefaultWorkflowId *uint `json:"defaultWorkflowId" validate:"omitempty,min=1"`
|
||||
AutoPublishArticles bool `json:"autoPublishArticles"`
|
||||
ApprovalExemptUsers []uint `json:"approvalExemptUsers" validate:"omitempty,dive,min=1"`
|
||||
ApprovalExemptRoles []uint `json:"approvalExemptRoles" validate:"omitempty,dive,min=1"`
|
||||
ApprovalExemptCategories []uint `json:"approvalExemptCategories" validate:"omitempty,dive,min=1"`
|
||||
RequireApprovalFor []string `json:"requireApprovalFor" validate:"omitempty,dive,min=1"`
|
||||
SkipApprovalFor []string `json:"skipApprovalFor" validate:"omitempty,dive,min=1"`
|
||||
IsActive bool `json:"isActive"`
|
||||
}
|
||||
|
||||
// UpdateClientApprovalSettingsRequest represents request for updating client approval settings
|
||||
type UpdateClientApprovalSettingsRequest struct {
|
||||
RequiresApproval *bool `json:"requiresApproval"`
|
||||
DefaultWorkflowId **uint `json:"defaultWorkflowId"` // double pointer to allow nil
|
||||
AutoPublishArticles *bool `json:"autoPublishArticles"`
|
||||
ApprovalExemptUsers []uint `json:"approvalExemptUsers" validate:"omitempty,dive,min=1"`
|
||||
ApprovalExemptRoles []uint `json:"approvalExemptRoles" validate:"omitempty,dive,min=1"`
|
||||
ApprovalExemptCategories []uint `json:"approvalExemptCategories" validate:"omitempty,dive,min=1"`
|
||||
RequireApprovalFor []string `json:"requireApprovalFor" validate:"omitempty,dive,min=1"`
|
||||
SkipApprovalFor []string `json:"skipApprovalFor" validate:"omitempty,dive,min=1"`
|
||||
IsActive *bool `json:"isActive"`
|
||||
}
|
||||
|
||||
// ToggleApprovalRequest represents request for toggling approval requirement
|
||||
type ToggleApprovalRequest struct {
|
||||
RequiresApproval bool `json:"requiresApproval"`
|
||||
}
|
||||
|
||||
// EnableApprovalRequest represents request for enabling approval with smooth transition
|
||||
type EnableApprovalRequest struct {
|
||||
DefaultWorkflowId *uint `json:"defaultWorkflowId" validate:"omitempty,min=1"`
|
||||
Reason string `json:"reason" validate:"omitempty,max=500"`
|
||||
}
|
||||
|
||||
// DisableApprovalRequest represents request for disabling approval system
|
||||
type DisableApprovalRequest struct {
|
||||
Reason string `json:"reason" validate:"required,max=500"`
|
||||
HandleAction string `json:"handleAction" validate:"required,oneof=auto_approve keep_pending reset_to_draft"` // How to handle pending articles
|
||||
}
|
||||
|
||||
// SetDefaultWorkflowRequest represents request for setting default workflow
|
||||
type SetDefaultWorkflowRequest struct {
|
||||
WorkflowId *uint `json:"workflowId" validate:"omitempty,min=1"`
|
||||
}
|
||||
|
||||
// AddExemptUserRequest represents request for adding user to exemption
|
||||
type AddExemptUserRequest struct {
|
||||
UserId uint `json:"userId" validate:"required,min=1"`
|
||||
Reason string `json:"reason" validate:"omitempty,max=500"`
|
||||
}
|
||||
|
||||
// RemoveExemptUserRequest represents request for removing user from exemption
|
||||
type RemoveExemptUserRequest struct {
|
||||
UserId uint `json:"userId" validate:"required,min=1"`
|
||||
Reason string `json:"reason" validate:"omitempty,max=500"`
|
||||
}
|
||||
|
||||
// AddExemptRoleRequest represents request for adding role to exemption
|
||||
type AddExemptRoleRequest struct {
|
||||
RoleId uint `json:"roleId" validate:"required,min=1"`
|
||||
Reason string `json:"reason" validate:"omitempty,max=500"`
|
||||
}
|
||||
|
||||
// RemoveExemptRoleRequest represents request for removing role from exemption
|
||||
type RemoveExemptRoleRequest struct {
|
||||
RoleId uint `json:"roleId" validate:"required,min=1"`
|
||||
Reason string `json:"reason" validate:"omitempty,max=500"`
|
||||
}
|
||||
|
||||
// AddExemptCategoryRequest represents request for adding category to exemption
|
||||
type AddExemptCategoryRequest struct {
|
||||
CategoryId uint `json:"categoryId" validate:"required,min=1"`
|
||||
Reason string `json:"reason" validate:"omitempty,max=500"`
|
||||
}
|
||||
|
||||
// RemoveExemptCategoryRequest represents request for removing category from exemption
|
||||
type RemoveExemptCategoryRequest struct {
|
||||
CategoryId uint `json:"categoryId" validate:"required,min=1"`
|
||||
Reason string `json:"reason" validate:"omitempty,max=500"`
|
||||
}
|
||||
|
|
@ -0,0 +1,82 @@
|
|||
package response
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
type ClientApprovalSettingsResponse struct {
|
||||
ID uint `json:"id"`
|
||||
ClientId string `json:"clientId"`
|
||||
RequiresApproval bool `json:"requiresApproval"`
|
||||
DefaultWorkflowId *uint `json:"defaultWorkflowId,omitempty"`
|
||||
AutoPublishArticles bool `json:"autoPublishArticles"`
|
||||
ApprovalExemptUsers []uint `json:"approvalExemptUsers"`
|
||||
ApprovalExemptRoles []uint `json:"approvalExemptRoles"`
|
||||
ApprovalExemptCategories []uint `json:"approvalExemptCategories"`
|
||||
RequireApprovalFor []string `json:"requireApprovalFor"`
|
||||
SkipApprovalFor []string `json:"skipApprovalFor"`
|
||||
IsActive bool `json:"isActive"`
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
UpdatedAt time.Time `json:"updatedAt"`
|
||||
}
|
||||
|
||||
type ClientApprovalSettingsDetailResponse struct {
|
||||
ID uint `json:"id"`
|
||||
ClientId string `json:"clientId"`
|
||||
RequiresApproval bool `json:"requiresApproval"`
|
||||
DefaultWorkflowId *uint `json:"defaultWorkflowId,omitempty"`
|
||||
AutoPublishArticles bool `json:"autoPublishArticles"`
|
||||
ApprovalExemptUsers []uint `json:"approvalExemptUsers"`
|
||||
ApprovalExemptRoles []uint `json:"approvalExemptRoles"`
|
||||
ApprovalExemptCategories []uint `json:"approvalExemptCategories"`
|
||||
RequireApprovalFor []string `json:"requireApprovalFor"`
|
||||
SkipApprovalFor []string `json:"skipApprovalFor"`
|
||||
IsActive bool `json:"isActive"`
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
UpdatedAt time.Time `json:"updatedAt"`
|
||||
|
||||
// Relations
|
||||
Client *ClientResponse `json:"client,omitempty"`
|
||||
Workflow *WorkflowResponse `json:"workflow,omitempty"`
|
||||
}
|
||||
|
||||
type ClientResponse struct {
|
||||
ID uint `json:"id"`
|
||||
Name string `json:"name"`
|
||||
IsActive bool `json:"isActive"`
|
||||
}
|
||||
|
||||
type WorkflowResponse struct {
|
||||
ID uint `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Description *string `json:"description,omitempty"`
|
||||
IsActive bool `json:"isActive"`
|
||||
}
|
||||
|
||||
type ApprovalStatusResponse struct {
|
||||
RequiresApproval bool `json:"requiresApproval"`
|
||||
AutoPublishArticles bool `json:"autoPublishArticles"`
|
||||
IsActive bool `json:"isActive"`
|
||||
Message string `json:"message,omitempty"`
|
||||
}
|
||||
|
||||
type ExemptUserResponse struct {
|
||||
UserId uint `json:"userId"`
|
||||
Reason string `json:"reason,omitempty"`
|
||||
}
|
||||
|
||||
type ExemptRoleResponse struct {
|
||||
RoleId uint `json:"roleId"`
|
||||
Reason string `json:"reason,omitempty"`
|
||||
}
|
||||
|
||||
type ExemptCategoryResponse struct {
|
||||
CategoryId uint `json:"categoryId"`
|
||||
Reason string `json:"reason,omitempty"`
|
||||
}
|
||||
|
||||
type ApprovalTransitionResponse struct {
|
||||
Success bool `json:"success"`
|
||||
Message string `json:"message"`
|
||||
AffectedArticles int `json:"affectedArticles,omitempty"`
|
||||
}
|
||||
|
|
@ -0,0 +1,326 @@
|
|||
package service
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"jaecoo-be/app/database/entity"
|
||||
"jaecoo-be/app/module/client_approval_settings/mapper"
|
||||
"jaecoo-be/app/module/client_approval_settings/repository"
|
||||
"jaecoo-be/app/module/client_approval_settings/request"
|
||||
"jaecoo-be/app/module/client_approval_settings/response"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/rs/zerolog"
|
||||
)
|
||||
|
||||
type clientApprovalSettingsService struct {
|
||||
clientApprovalSettingsRepo repository.ClientApprovalSettingsRepository
|
||||
Log zerolog.Logger
|
||||
}
|
||||
|
||||
type ClientApprovalSettingsService interface {
|
||||
GetByClientId(clientId *uuid.UUID) (*response.ClientApprovalSettingsResponse, error)
|
||||
Create(clientId *uuid.UUID, req request.CreateClientApprovalSettingsRequest) (*response.ClientApprovalSettingsResponse, error)
|
||||
Update(clientId *uuid.UUID, req request.UpdateClientApprovalSettingsRequest) (*response.ClientApprovalSettingsResponse, error)
|
||||
Delete(clientId *uuid.UUID) error
|
||||
ToggleApprovalRequirement(clientId *uuid.UUID, requiresApproval bool) error
|
||||
SetDefaultWorkflow(clientId *uuid.UUID, workflowId *uint) error
|
||||
AddExemptUser(clientId *uuid.UUID, userId uint) error
|
||||
RemoveExemptUser(clientId *uuid.UUID, userId uint) error
|
||||
AddExemptRole(clientId *uuid.UUID, roleId uint) error
|
||||
RemoveExemptRole(clientId *uuid.UUID, roleId uint) error
|
||||
AddExemptCategory(clientId *uuid.UUID, categoryId uint) error
|
||||
RemoveExemptCategory(clientId *uuid.UUID, categoryId uint) error
|
||||
CheckIfApprovalRequired(clientId *uuid.UUID, userId uint, userLevelId uint, categoryId uint, contentType string) (bool, error)
|
||||
|
||||
// Enhanced methods for dynamic approval management
|
||||
EnableApprovalWithTransition(clientId *uuid.UUID, defaultWorkflowId *uint) error
|
||||
DisableApprovalWithAutoPublish(clientId *uuid.UUID, reason string) error
|
||||
HandlePendingApprovalsOnDisable(clientId *uuid.UUID, action string) error // "auto_approve", "keep_pending", "reset_to_draft"
|
||||
}
|
||||
|
||||
func NewClientApprovalSettingsService(
|
||||
clientApprovalSettingsRepo repository.ClientApprovalSettingsRepository,
|
||||
log zerolog.Logger,
|
||||
) ClientApprovalSettingsService {
|
||||
return &clientApprovalSettingsService{
|
||||
clientApprovalSettingsRepo: clientApprovalSettingsRepo,
|
||||
Log: log,
|
||||
}
|
||||
}
|
||||
|
||||
func (_i *clientApprovalSettingsService) GetByClientId(clientId *uuid.UUID) (*response.ClientApprovalSettingsResponse, error) {
|
||||
settings, err := _i.clientApprovalSettingsRepo.FindByClientId(*clientId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if settings == nil {
|
||||
// Return default settings if none found
|
||||
return &response.ClientApprovalSettingsResponse{
|
||||
ClientId: clientId.String(),
|
||||
RequiresApproval: true, // Default to requiring approval
|
||||
AutoPublishArticles: false,
|
||||
ApprovalExemptUsers: []uint{},
|
||||
ApprovalExemptRoles: []uint{},
|
||||
ApprovalExemptCategories: []uint{},
|
||||
RequireApprovalFor: []string{},
|
||||
SkipApprovalFor: []string{},
|
||||
IsActive: true,
|
||||
}, nil
|
||||
}
|
||||
|
||||
return mapper.ClientApprovalSettingsResponseMapper(_i.Log, clientId, settings), nil
|
||||
}
|
||||
|
||||
func (_i *clientApprovalSettingsService) Create(clientId *uuid.UUID, req request.CreateClientApprovalSettingsRequest) (*response.ClientApprovalSettingsResponse, error) {
|
||||
// Check if settings already exist
|
||||
existing, err := _i.clientApprovalSettingsRepo.FindByClientId(*clientId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if existing != nil {
|
||||
return nil, fmt.Errorf("approval settings already exist for this client")
|
||||
}
|
||||
|
||||
// Create new settings
|
||||
settings := &entity.ClientApprovalSettings{
|
||||
RequiresApproval: &req.RequiresApproval,
|
||||
DefaultWorkflowId: req.DefaultWorkflowId,
|
||||
AutoPublishArticles: &req.AutoPublishArticles,
|
||||
ApprovalExemptUsers: req.ApprovalExemptUsers,
|
||||
ApprovalExemptRoles: req.ApprovalExemptRoles,
|
||||
ApprovalExemptCategories: req.ApprovalExemptCategories,
|
||||
RequireApprovalFor: req.RequireApprovalFor,
|
||||
SkipApprovalFor: req.SkipApprovalFor,
|
||||
IsActive: &req.IsActive,
|
||||
}
|
||||
|
||||
createdSettings, err := _i.clientApprovalSettingsRepo.Create(clientId, settings)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return mapper.ClientApprovalSettingsResponseMapper(_i.Log, clientId, createdSettings), nil
|
||||
}
|
||||
|
||||
func (_i *clientApprovalSettingsService) Update(clientId *uuid.UUID, req request.UpdateClientApprovalSettingsRequest) (*response.ClientApprovalSettingsResponse, error) {
|
||||
// Get existing settings
|
||||
settings, err := _i.clientApprovalSettingsRepo.FindByClientId(*clientId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if settings == nil {
|
||||
return nil, fmt.Errorf("approval settings not found for this client")
|
||||
}
|
||||
|
||||
// Update fields if provided
|
||||
if req.RequiresApproval != nil {
|
||||
settings.RequiresApproval = req.RequiresApproval
|
||||
}
|
||||
if req.DefaultWorkflowId != nil {
|
||||
settings.DefaultWorkflowId = *req.DefaultWorkflowId
|
||||
}
|
||||
if req.AutoPublishArticles != nil {
|
||||
settings.AutoPublishArticles = req.AutoPublishArticles
|
||||
}
|
||||
if req.ApprovalExemptUsers != nil {
|
||||
settings.ApprovalExemptUsers = req.ApprovalExemptUsers
|
||||
}
|
||||
if req.ApprovalExemptRoles != nil {
|
||||
settings.ApprovalExemptRoles = req.ApprovalExemptRoles
|
||||
}
|
||||
if req.ApprovalExemptCategories != nil {
|
||||
settings.ApprovalExemptCategories = req.ApprovalExemptCategories
|
||||
}
|
||||
if req.RequireApprovalFor != nil {
|
||||
settings.RequireApprovalFor = req.RequireApprovalFor
|
||||
}
|
||||
if req.SkipApprovalFor != nil {
|
||||
settings.SkipApprovalFor = req.SkipApprovalFor
|
||||
}
|
||||
if req.IsActive != nil {
|
||||
settings.IsActive = req.IsActive
|
||||
}
|
||||
|
||||
updatedSettings, err := _i.clientApprovalSettingsRepo.Update(clientId, settings)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return mapper.ClientApprovalSettingsResponseMapper(_i.Log, clientId, updatedSettings), nil
|
||||
}
|
||||
|
||||
func (_i *clientApprovalSettingsService) Delete(clientId *uuid.UUID) error {
|
||||
return _i.clientApprovalSettingsRepo.Delete(clientId)
|
||||
}
|
||||
|
||||
func (_i *clientApprovalSettingsService) ToggleApprovalRequirement(clientId *uuid.UUID, requiresApproval bool) error {
|
||||
settings, err := _i.clientApprovalSettingsRepo.FindByClientId(*clientId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if settings == nil {
|
||||
return fmt.Errorf("approval settings not found for this client")
|
||||
}
|
||||
|
||||
settings.RequiresApproval = &requiresApproval
|
||||
_, err = _i.clientApprovalSettingsRepo.Update(clientId, settings)
|
||||
return err
|
||||
}
|
||||
|
||||
func (_i *clientApprovalSettingsService) SetDefaultWorkflow(clientId *uuid.UUID, workflowId *uint) error {
|
||||
settings, err := _i.clientApprovalSettingsRepo.FindByClientId(*clientId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if settings == nil {
|
||||
return fmt.Errorf("approval settings not found for this client")
|
||||
}
|
||||
|
||||
settings.DefaultWorkflowId = workflowId
|
||||
_, err = _i.clientApprovalSettingsRepo.Update(clientId, settings)
|
||||
return err
|
||||
}
|
||||
|
||||
func (_i *clientApprovalSettingsService) AddExemptUser(clientId *uuid.UUID, userId uint) error {
|
||||
return _i.clientApprovalSettingsRepo.AddExemptUser(clientId, userId)
|
||||
}
|
||||
|
||||
func (_i *clientApprovalSettingsService) RemoveExemptUser(clientId *uuid.UUID, userId uint) error {
|
||||
return _i.clientApprovalSettingsRepo.RemoveExemptUser(clientId, userId)
|
||||
}
|
||||
|
||||
func (_i *clientApprovalSettingsService) AddExemptRole(clientId *uuid.UUID, roleId uint) error {
|
||||
return _i.clientApprovalSettingsRepo.AddExemptRole(clientId, roleId)
|
||||
}
|
||||
|
||||
func (_i *clientApprovalSettingsService) RemoveExemptRole(clientId *uuid.UUID, roleId uint) error {
|
||||
return _i.clientApprovalSettingsRepo.RemoveExemptRole(clientId, roleId)
|
||||
}
|
||||
|
||||
func (_i *clientApprovalSettingsService) AddExemptCategory(clientId *uuid.UUID, categoryId uint) error {
|
||||
return _i.clientApprovalSettingsRepo.AddExemptCategory(clientId, categoryId)
|
||||
}
|
||||
|
||||
func (_i *clientApprovalSettingsService) RemoveExemptCategory(clientId *uuid.UUID, categoryId uint) error {
|
||||
return _i.clientApprovalSettingsRepo.RemoveExemptCategory(clientId, categoryId)
|
||||
}
|
||||
|
||||
func (_i *clientApprovalSettingsService) CheckIfApprovalRequired(clientId *uuid.UUID, userId uint, userLevelId uint, categoryId uint, contentType string) (bool, error) {
|
||||
settings, err := _i.clientApprovalSettingsRepo.FindActiveSettings(clientId)
|
||||
if err != nil {
|
||||
return true, err // Default to requiring approval on error
|
||||
}
|
||||
|
||||
if settings == nil {
|
||||
return true, nil // Default to requiring approval if no settings
|
||||
}
|
||||
|
||||
// Check if approval is disabled
|
||||
if settings.RequiresApproval != nil && !*settings.RequiresApproval {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// Check user exemption
|
||||
for _, exemptUserId := range settings.ApprovalExemptUsers {
|
||||
if exemptUserId == userId {
|
||||
return false, nil
|
||||
}
|
||||
}
|
||||
|
||||
// Check role exemption
|
||||
for _, exemptRoleId := range settings.ApprovalExemptRoles {
|
||||
if exemptRoleId == userLevelId {
|
||||
return false, nil
|
||||
}
|
||||
}
|
||||
|
||||
// Check category exemption
|
||||
for _, exemptCategoryId := range settings.ApprovalExemptCategories {
|
||||
if exemptCategoryId == categoryId {
|
||||
return false, nil
|
||||
}
|
||||
}
|
||||
|
||||
// Check content type exemptions
|
||||
for _, skipType := range settings.SkipApprovalFor {
|
||||
if skipType == contentType {
|
||||
return false, nil
|
||||
}
|
||||
}
|
||||
|
||||
// Check if content type requires approval
|
||||
for _, requireType := range settings.RequireApprovalFor {
|
||||
if requireType == contentType {
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
|
||||
// Default to requiring approval
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func (_i *clientApprovalSettingsService) EnableApprovalWithTransition(clientId *uuid.UUID, defaultWorkflowId *uint) error {
|
||||
settings, err := _i.clientApprovalSettingsRepo.FindByClientId(*clientId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if settings == nil {
|
||||
// Create new settings
|
||||
settings = &entity.ClientApprovalSettings{
|
||||
RequiresApproval: &[]bool{true}[0],
|
||||
DefaultWorkflowId: defaultWorkflowId,
|
||||
AutoPublishArticles: &[]bool{false}[0],
|
||||
IsActive: &[]bool{true}[0],
|
||||
}
|
||||
_, err = _i.clientApprovalSettingsRepo.Create(clientId, settings)
|
||||
} else {
|
||||
// Update existing settings
|
||||
settings.RequiresApproval = &[]bool{true}[0]
|
||||
settings.DefaultWorkflowId = defaultWorkflowId
|
||||
settings.AutoPublishArticles = &[]bool{false}[0]
|
||||
settings.IsActive = &[]bool{true}[0]
|
||||
_, err = _i.clientApprovalSettingsRepo.Update(clientId, settings)
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func (_i *clientApprovalSettingsService) DisableApprovalWithAutoPublish(clientId *uuid.UUID, reason string) error {
|
||||
settings, err := _i.clientApprovalSettingsRepo.FindByClientId(*clientId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if settings == nil {
|
||||
return fmt.Errorf("approval settings not found for this client")
|
||||
}
|
||||
|
||||
settings.RequiresApproval = &[]bool{false}[0]
|
||||
settings.AutoPublishArticles = &[]bool{true}[0]
|
||||
settings.IsActive = &[]bool{true}[0]
|
||||
|
||||
_, err = _i.clientApprovalSettingsRepo.Update(clientId, settings)
|
||||
return err
|
||||
}
|
||||
|
||||
func (_i *clientApprovalSettingsService) HandlePendingApprovalsOnDisable(clientId *uuid.UUID, action string) error {
|
||||
// This would typically interact with article approval flows
|
||||
// For now, just log the action
|
||||
_i.Log.Info().
|
||||
Str("client_id", clientId.String()).
|
||||
Str("action", action).
|
||||
Msg("Handling pending approvals on disable")
|
||||
|
||||
// TODO: Implement actual logic based on action:
|
||||
// - "auto_approve": Auto approve all pending articles
|
||||
// - "keep_pending": Keep articles in pending state
|
||||
// - "reset_to_draft": Reset articles to draft state
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
@ -0,0 +1,54 @@
|
|||
package clients
|
||||
|
||||
import (
|
||||
"jaecoo-be/app/module/clients/controller"
|
||||
"jaecoo-be/app/module/clients/repository"
|
||||
"jaecoo-be/app/module/clients/service"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"go.uber.org/fx"
|
||||
)
|
||||
|
||||
// struct of ClientsRouter
|
||||
type ClientsRouter struct {
|
||||
App fiber.Router
|
||||
Controller *controller.Controller
|
||||
}
|
||||
|
||||
// register bulky of Clients module
|
||||
var NewClientsModule = fx.Options(
|
||||
// register repository of Clients module
|
||||
fx.Provide(repository.NewClientsRepository),
|
||||
|
||||
// register service of Clients module
|
||||
fx.Provide(service.NewClientsService),
|
||||
|
||||
// register controller of Clients module
|
||||
fx.Provide(controller.NewController),
|
||||
|
||||
// register router of Clients module
|
||||
fx.Provide(NewClientsRouter),
|
||||
)
|
||||
|
||||
// init ClientsRouter
|
||||
func NewClientsRouter(fiber *fiber.App, controller *controller.Controller) *ClientsRouter {
|
||||
return &ClientsRouter{
|
||||
App: fiber,
|
||||
Controller: controller,
|
||||
}
|
||||
}
|
||||
|
||||
// register routes of Clients module
|
||||
func (_i *ClientsRouter) RegisterClientsRoutes() {
|
||||
// define controllers
|
||||
clientsController := _i.Controller.Clients
|
||||
|
||||
// define routes
|
||||
_i.App.Route("/clients", func(router fiber.Router) {
|
||||
router.Get("/", clientsController.All)
|
||||
router.Get("/:id", clientsController.Show)
|
||||
router.Post("/", clientsController.Save)
|
||||
router.Put("/:id", clientsController.Update)
|
||||
router.Delete("/:id", clientsController.Delete)
|
||||
})
|
||||
}
|
||||
|
|
@ -0,0 +1,197 @@
|
|||
package controller
|
||||
|
||||
import (
|
||||
"jaecoo-be/app/module/clients/request"
|
||||
"jaecoo-be/app/module/clients/service"
|
||||
"jaecoo-be/utils/paginator"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"github.com/google/uuid"
|
||||
"github.com/rs/zerolog"
|
||||
|
||||
utilRes "jaecoo-be/utils/response"
|
||||
utilVal "jaecoo-be/utils/validator"
|
||||
)
|
||||
|
||||
type clientsController struct {
|
||||
clientsService service.ClientsService
|
||||
Log zerolog.Logger
|
||||
}
|
||||
|
||||
type ClientsController interface {
|
||||
All(c *fiber.Ctx) error
|
||||
Show(c *fiber.Ctx) error
|
||||
Save(c *fiber.Ctx) error
|
||||
Update(c *fiber.Ctx) error
|
||||
Delete(c *fiber.Ctx) error
|
||||
}
|
||||
|
||||
func NewClientsController(clientsService service.ClientsService, log zerolog.Logger) ClientsController {
|
||||
return &clientsController{
|
||||
clientsService: clientsService,
|
||||
Log: log,
|
||||
}
|
||||
}
|
||||
|
||||
// All get all Clients
|
||||
// @Summary Get all Clients
|
||||
// @Description API for getting all Clients
|
||||
// @Tags Clients
|
||||
// @Security Bearer
|
||||
// @Param req query request.ClientsQueryRequest false "query parameters"
|
||||
// @Param req query paginator.Pagination false "pagination parameters"
|
||||
// @Success 200 {object} response.Response
|
||||
// @Failure 400 {object} response.BadRequestError
|
||||
// @Failure 401 {object} response.UnauthorizedError
|
||||
// @Failure 500 {object} response.InternalServerError
|
||||
// @Router /clients [get]
|
||||
func (_i *clientsController) All(c *fiber.Ctx) error {
|
||||
paginate, err := paginator.Paginate(c)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
reqContext := request.ClientsQueryRequestContext{
|
||||
Name: c.Query("name"),
|
||||
CreatedById: c.Query("createdById"),
|
||||
}
|
||||
req := reqContext.ToParamRequest()
|
||||
req.Pagination = paginate
|
||||
|
||||
clientsData, paging, err := _i.clientsService.All(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return utilRes.Resp(c, utilRes.Response{
|
||||
Success: true,
|
||||
Messages: utilRes.Messages{"Clients list successfully retrieved"},
|
||||
Data: clientsData,
|
||||
Meta: paging,
|
||||
})
|
||||
}
|
||||
|
||||
// Show get one Clients
|
||||
// @Summary Get one Clients
|
||||
// @Description API for getting one Clients
|
||||
// @Tags Clients
|
||||
// @Security Bearer
|
||||
// @Param id path int true "Clients ID"
|
||||
// @Success 200 {object} response.Response
|
||||
// @Failure 400 {object} response.BadRequestError
|
||||
// @Failure 401 {object} response.UnauthorizedError
|
||||
// @Failure 500 {object} response.InternalServerError
|
||||
// @Router /clients/{id} [get]
|
||||
func (_i *clientsController) Show(c *fiber.Ctx) error {
|
||||
idStr := c.Params("id")
|
||||
id, err := uuid.Parse(idStr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
clientsData, err := _i.clientsService.Show(id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return utilRes.Resp(c, utilRes.Response{
|
||||
Success: true,
|
||||
Messages: utilRes.Messages{"Clients successfully retrieved"},
|
||||
Data: clientsData,
|
||||
})
|
||||
}
|
||||
|
||||
// Save create Clients
|
||||
// @Summary Create Clients
|
||||
// @Description API for create Clients
|
||||
// @Tags Clients
|
||||
// @Security Bearer
|
||||
// @Param Authorization header string false "Insert your access token" default(Bearer <Add access token here>)
|
||||
// @Param payload body request.ClientsCreateRequest true "Required payload"
|
||||
// @Success 200 {object} response.Response
|
||||
// @Failure 400 {object} response.BadRequestError
|
||||
// @Failure 401 {object} response.UnauthorizedError
|
||||
// @Failure 500 {object} response.InternalServerError
|
||||
// @Router /clients [post]
|
||||
func (_i *clientsController) Save(c *fiber.Ctx) error {
|
||||
req := new(request.ClientsCreateRequest)
|
||||
if err := utilVal.ParseAndValidate(c, req); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
authToken := c.Get("Authorization")
|
||||
dataResult, err := _i.clientsService.Save(*req, authToken)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return utilRes.Resp(c, utilRes.Response{
|
||||
Success: true,
|
||||
Messages: utilRes.Messages{"Clients successfully created"},
|
||||
Data: dataResult,
|
||||
})
|
||||
}
|
||||
|
||||
// Update update Clients
|
||||
// @Summary update Clients
|
||||
// @Description API for update Clients
|
||||
// @Tags Clients
|
||||
// @Security Bearer
|
||||
// @Param payload body request.ClientsUpdateRequest true "Required payload"
|
||||
// @Param id path string true "Clients ID"
|
||||
// @Success 200 {object} response.Response
|
||||
// @Failure 400 {object} response.BadRequestError
|
||||
// @Failure 401 {object} response.UnauthorizedError
|
||||
// @Failure 500 {object} response.InternalServerError
|
||||
// @Router /clients/{id} [put]
|
||||
func (_i *clientsController) Update(c *fiber.Ctx) error {
|
||||
idStr := c.Params("id")
|
||||
id, err := uuid.Parse(idStr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
req := new(request.ClientsUpdateRequest)
|
||||
if err := utilVal.ParseAndValidate(c, req); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = _i.clientsService.Update(id, *req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return utilRes.Resp(c, utilRes.Response{
|
||||
Success: true,
|
||||
Messages: utilRes.Messages{"Clients successfully updated"},
|
||||
})
|
||||
}
|
||||
|
||||
// Delete delete Clients
|
||||
// @Summary delete Clients
|
||||
// @Description API for delete Clients
|
||||
// @Tags Clients
|
||||
// @Security Bearer
|
||||
// @Param id path string true "Clients ID"
|
||||
// @Success 200 {object} response.Response
|
||||
// @Failure 400 {object} response.BadRequestError
|
||||
// @Failure 401 {object} response.UnauthorizedError
|
||||
// @Failure 500 {object} response.InternalServerError
|
||||
// @Router /clients/{id} [delete]
|
||||
func (_i *clientsController) Delete(c *fiber.Ctx) error {
|
||||
idStr := c.Params("id")
|
||||
id, err := uuid.Parse(idStr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = _i.clientsService.Delete(id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return utilRes.Resp(c, utilRes.Response{
|
||||
Success: true,
|
||||
Messages: utilRes.Messages{"Clients successfully deleted"},
|
||||
})
|
||||
}
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
package controller
|
||||
|
||||
import (
|
||||
"jaecoo-be/app/module/clients/service"
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
)
|
||||
|
||||
type Controller struct {
|
||||
Clients ClientsController
|
||||
}
|
||||
|
||||
func NewController(ClientsService service.ClientsService, log zerolog.Logger) *Controller {
|
||||
return &Controller{
|
||||
Clients: NewClientsController(ClientsService, log),
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
package mapper
|
||||
|
||||
import (
|
||||
"jaecoo-be/app/database/entity"
|
||||
res "jaecoo-be/app/module/clients/response"
|
||||
)
|
||||
|
||||
func ClientsResponseMapper(clientsReq *entity.Clients) (clientsRes *res.ClientsResponse) {
|
||||
if clientsReq != nil {
|
||||
clientsRes = &res.ClientsResponse{
|
||||
ClientID: clientsReq.ID,
|
||||
Name: clientsReq.Name,
|
||||
CreatedById: *clientsReq.CreatedById,
|
||||
IsActive: *clientsReq.IsActive,
|
||||
CreatedAt: clientsReq.CreatedAt,
|
||||
UpdatedAt: clientsReq.UpdatedAt,
|
||||
}
|
||||
}
|
||||
return clientsRes
|
||||
}
|
||||
|
|
@ -0,0 +1,94 @@
|
|||
package repository
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"jaecoo-be/app/database"
|
||||
"jaecoo-be/app/database/entity"
|
||||
"jaecoo-be/app/module/clients/request"
|
||||
"jaecoo-be/utils/paginator"
|
||||
"strings"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/rs/zerolog"
|
||||
)
|
||||
|
||||
type clientsRepository struct {
|
||||
DB *database.Database
|
||||
Log zerolog.Logger
|
||||
}
|
||||
|
||||
// ClientsRepository define interface of IClientsRepository
|
||||
type ClientsRepository interface {
|
||||
GetAll(req request.ClientsQueryRequest) (clientss []*entity.Clients, paging paginator.Pagination, err error)
|
||||
FindOne(id uuid.UUID) (clients *entity.Clients, err error)
|
||||
Create(clients *entity.Clients) (clientsReturn *entity.Clients, err error)
|
||||
Update(id uuid.UUID, clients *entity.Clients) (err error)
|
||||
Delete(id uuid.UUID) (err error)
|
||||
}
|
||||
|
||||
func NewClientsRepository(db *database.Database, logger zerolog.Logger) ClientsRepository {
|
||||
return &clientsRepository{
|
||||
DB: db,
|
||||
Log: logger,
|
||||
}
|
||||
}
|
||||
|
||||
// implement interface of IClientsRepository
|
||||
func (_i *clientsRepository) GetAll(req request.ClientsQueryRequest) (clientss []*entity.Clients, paging paginator.Pagination, err error) {
|
||||
var count int64
|
||||
|
||||
query := _i.DB.DB.Model(&entity.Clients{})
|
||||
query = query.Where("is_active = ?", true)
|
||||
|
||||
if req.Name != nil && *req.Name != "" {
|
||||
name := strings.ToLower(*req.Name)
|
||||
query = query.Where("LOWER(name) LIKE ?", "%"+strings.ToLower(name)+"%")
|
||||
}
|
||||
if req.CreatedById != nil {
|
||||
query = query.Where("created_by_id = ?", req.CreatedById)
|
||||
}
|
||||
query.Count(&count)
|
||||
|
||||
if req.Pagination.SortBy != "" {
|
||||
direction := "ASC"
|
||||
if req.Pagination.Sort == "desc" {
|
||||
direction = "DESC"
|
||||
}
|
||||
query.Order(fmt.Sprintf("%s %s", req.Pagination.SortBy, direction))
|
||||
}
|
||||
|
||||
req.Pagination.Count = count
|
||||
req.Pagination = paginator.Paging(req.Pagination)
|
||||
|
||||
err = query.Offset(req.Pagination.Offset).Limit(req.Pagination.Limit).Find(&clientss).Error
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
paging = *req.Pagination
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (_i *clientsRepository) FindOne(id uuid.UUID) (clients *entity.Clients, err error) {
|
||||
if err := _i.DB.DB.First(&clients, id).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return clients, nil
|
||||
}
|
||||
|
||||
func (_i *clientsRepository) Create(clients *entity.Clients) (clientsReturn *entity.Clients, err error) {
|
||||
result := _i.DB.DB.Create(clients)
|
||||
return clients, result.Error
|
||||
}
|
||||
|
||||
func (_i *clientsRepository) Update(id uuid.UUID, clients *entity.Clients) (err error) {
|
||||
return _i.DB.DB.Model(&entity.Clients{}).
|
||||
Where(&entity.Clients{ID: id}).
|
||||
Updates(clients).Error
|
||||
}
|
||||
|
||||
func (_i *clientsRepository) Delete(id uuid.UUID) error {
|
||||
return _i.DB.DB.Delete(&entity.Clients{}, id).Error
|
||||
}
|
||||
|
|
@ -0,0 +1,66 @@
|
|||
package request
|
||||
|
||||
import (
|
||||
"jaecoo-be/app/database/entity"
|
||||
"jaecoo-be/utils/paginator"
|
||||
"strconv"
|
||||
"time"
|
||||
)
|
||||
|
||||
type ClientsGeneric interface {
|
||||
ToEntity()
|
||||
}
|
||||
|
||||
type ClientsQueryRequest struct {
|
||||
Name *string `json:"name"`
|
||||
CreatedById *uint `json:"createdBy"`
|
||||
Pagination *paginator.Pagination `json:"pagination"`
|
||||
}
|
||||
|
||||
type ClientsCreateRequest struct {
|
||||
Name string `json:"name" validate:"required"`
|
||||
CreatedById *uint `json:"createdById"`
|
||||
}
|
||||
|
||||
func (req ClientsCreateRequest) ToEntity() *entity.Clients {
|
||||
return &entity.Clients{
|
||||
Name: req.Name,
|
||||
CreatedById: req.CreatedById,
|
||||
CreatedAt: time.Now(),
|
||||
}
|
||||
}
|
||||
|
||||
type ClientsUpdateRequest struct {
|
||||
Name string `json:"name" validate:"required"`
|
||||
CreatedById *uint `json:"createdById"`
|
||||
}
|
||||
|
||||
func (req ClientsUpdateRequest) ToEntity() *entity.Clients {
|
||||
return &entity.Clients{
|
||||
Name: req.Name,
|
||||
CreatedById: req.CreatedById,
|
||||
UpdatedAt: time.Now(),
|
||||
}
|
||||
}
|
||||
|
||||
type ClientsQueryRequestContext struct {
|
||||
Name string `json:"name"`
|
||||
CreatedById string `json:"createdById"`
|
||||
}
|
||||
|
||||
func (req ClientsQueryRequestContext) ToParamRequest() ClientsQueryRequest {
|
||||
var request ClientsQueryRequest
|
||||
|
||||
if name := req.Name; name != "" {
|
||||
request.Name = &name
|
||||
}
|
||||
if createdByStr := req.CreatedById; createdByStr != "" {
|
||||
createdBy, err := strconv.Atoi(createdByStr)
|
||||
if err == nil {
|
||||
createdByIdUint := uint(createdBy)
|
||||
request.CreatedById = &createdByIdUint
|
||||
}
|
||||
}
|
||||
|
||||
return request
|
||||
}
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
package response
|
||||
|
||||
import (
|
||||
"github.com/google/uuid"
|
||||
"time"
|
||||
)
|
||||
|
||||
type ClientsResponse struct {
|
||||
ClientID uuid.UUID `json:"clientId"`
|
||||
Name string `json:"name"`
|
||||
CreatedById uint `json:"createdById"`
|
||||
IsActive bool `json:"isActive"`
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
UpdatedAt time.Time `json:"updatedAt"`
|
||||
}
|
||||
|
|
@ -0,0 +1,96 @@
|
|||
package service
|
||||
|
||||
import (
|
||||
"jaecoo-be/app/database/entity"
|
||||
"jaecoo-be/app/module/clients/mapper"
|
||||
"jaecoo-be/app/module/clients/repository"
|
||||
"jaecoo-be/app/module/clients/request"
|
||||
"jaecoo-be/app/module/clients/response"
|
||||
usersRepository "jaecoo-be/app/module/users/repository"
|
||||
"jaecoo-be/utils/paginator"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/rs/zerolog"
|
||||
|
||||
utilSvc "jaecoo-be/utils/service"
|
||||
)
|
||||
|
||||
// ClientsService
|
||||
type clientsService struct {
|
||||
Repo repository.ClientsRepository
|
||||
UsersRepo usersRepository.UsersRepository
|
||||
Log zerolog.Logger
|
||||
}
|
||||
|
||||
// ClientsService define interface of IClientsService
|
||||
type ClientsService interface {
|
||||
All(req request.ClientsQueryRequest) (clients []*response.ClientsResponse, paging paginator.Pagination, err error)
|
||||
Show(id uuid.UUID) (clients *response.ClientsResponse, err error)
|
||||
Save(req request.ClientsCreateRequest, authToken string) (clients *entity.Clients, err error)
|
||||
Update(id uuid.UUID, req request.ClientsUpdateRequest) (err error)
|
||||
Delete(id uuid.UUID) error
|
||||
}
|
||||
|
||||
// NewClientsService init ClientsService
|
||||
func NewClientsService(repo repository.ClientsRepository, log zerolog.Logger, usersRepo usersRepository.UsersRepository) ClientsService {
|
||||
|
||||
return &clientsService{
|
||||
Repo: repo,
|
||||
Log: log,
|
||||
UsersRepo: usersRepo,
|
||||
}
|
||||
}
|
||||
|
||||
// All implement interface of ClientsService
|
||||
func (_i *clientsService) All(req request.ClientsQueryRequest) (clientss []*response.ClientsResponse, paging paginator.Pagination, err error) {
|
||||
results, paging, err := _i.Repo.GetAll(req)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
for _, result := range results {
|
||||
clientss = append(clientss, mapper.ClientsResponseMapper(result))
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (_i *clientsService) Show(id uuid.UUID) (clients *response.ClientsResponse, err error) {
|
||||
result, err := _i.Repo.FindOne(id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return mapper.ClientsResponseMapper(result), nil
|
||||
}
|
||||
|
||||
func (_i *clientsService) Save(req request.ClientsCreateRequest, authToken string) (clients *entity.Clients, err error) {
|
||||
_i.Log.Info().Interface("data", req).Msg("")
|
||||
newReq := req.ToEntity()
|
||||
_i.Log.Info().Interface("token", authToken).Msg("")
|
||||
createdBy := utilSvc.GetUserInfo(_i.Log, _i.UsersRepo, authToken)
|
||||
_i.Log.Info().Interface("token", authToken).Msg("")
|
||||
newReq.CreatedById = &createdBy.ID
|
||||
|
||||
newReq.ID = uuid.New()
|
||||
|
||||
_i.Log.Info().Interface("new data", newReq).Msg("")
|
||||
|
||||
return _i.Repo.Create(newReq)
|
||||
}
|
||||
|
||||
func (_i *clientsService) Update(id uuid.UUID, req request.ClientsUpdateRequest) (err error) {
|
||||
_i.Log.Info().Interface("data", req).Msg("")
|
||||
return _i.Repo.Update(id, req.ToEntity())
|
||||
}
|
||||
|
||||
func (_i *clientsService) Delete(id uuid.UUID) error {
|
||||
result, err := _i.Repo.FindOne(id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
isActive := false
|
||||
result.IsActive = &isActive
|
||||
return _i.Repo.Update(id, result)
|
||||
}
|
||||
|
|
@ -10,6 +10,8 @@ import (
|
|||
"jaecoo-be/app/module/articles"
|
||||
"jaecoo-be/app/module/banners"
|
||||
"jaecoo-be/app/module/cities"
|
||||
"jaecoo-be/app/module/client_approval_settings"
|
||||
"jaecoo-be/app/module/clients"
|
||||
"jaecoo-be/app/module/custom_static_pages"
|
||||
"jaecoo-be/app/module/districts"
|
||||
"jaecoo-be/app/module/feedbacks"
|
||||
|
|
@ -44,6 +46,8 @@ type Router struct {
|
|||
ArticlesRouter *articles.ArticlesRouter
|
||||
BannersRouter *banners.BannersRouter
|
||||
CitiesRouter *cities.CitiesRouter
|
||||
ClientApprovalSettingsRouter *client_approval_settings.ClientApprovalSettingsRouter
|
||||
ClientsRouter *clients.ClientsRouter
|
||||
CustomStaticPagesRouter *custom_static_pages.CustomStaticPagesRouter
|
||||
DistrictsRouter *districts.DistrictsRouter
|
||||
FeedbacksRouter *feedbacks.FeedbacksRouter
|
||||
|
|
@ -73,6 +77,8 @@ func NewRouter(
|
|||
articlesRouter *articles.ArticlesRouter,
|
||||
bannersRouter *banners.BannersRouter,
|
||||
citiesRouter *cities.CitiesRouter,
|
||||
clientApprovalSettingsRouter *client_approval_settings.ClientApprovalSettingsRouter,
|
||||
clientsRouter *clients.ClientsRouter,
|
||||
customStaticPagesRouter *custom_static_pages.CustomStaticPagesRouter,
|
||||
districtsRouter *districts.DistrictsRouter,
|
||||
feedbacksRouter *feedbacks.FeedbacksRouter,
|
||||
|
|
@ -100,6 +106,8 @@ func NewRouter(
|
|||
ArticlesRouter: articlesRouter,
|
||||
BannersRouter: bannersRouter,
|
||||
CitiesRouter: citiesRouter,
|
||||
ClientApprovalSettingsRouter: clientApprovalSettingsRouter,
|
||||
ClientsRouter: clientsRouter,
|
||||
CustomStaticPagesRouter: customStaticPagesRouter,
|
||||
DistrictsRouter: districtsRouter,
|
||||
FeedbacksRouter: feedbacksRouter,
|
||||
|
|
@ -137,6 +145,8 @@ func (r *Router) Register() {
|
|||
r.ArticleCommentsRouter.RegisterArticleCommentsRoutes()
|
||||
r.BannersRouter.RegisterBannersRoutes()
|
||||
r.CitiesRouter.RegisterCitiesRoutes()
|
||||
r.ClientApprovalSettingsRouter.RegisterClientApprovalSettingsRoutes()
|
||||
r.ClientsRouter.RegisterClientsRoutes()
|
||||
r.CustomStaticPagesRouter.RegisterCustomStaticPagesRoutes()
|
||||
r.DistrictsRouter.RegisterDistrictsRoutes()
|
||||
r.FeedbacksRouter.RegisterFeedbacksRoutes()
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ port = ":8800"
|
|||
domain = "https://jaecoocihampelasbdg.com/api"
|
||||
external-port = ":8810"
|
||||
idle-timeout = 5 # As seconds
|
||||
print-routes = false
|
||||
print-routes = true
|
||||
prefork = true
|
||||
production = false
|
||||
body-limit = 1048576000 # "100 * 1024 * 1024"
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
4
main.go
4
main.go
|
|
@ -12,6 +12,8 @@ import (
|
|||
"jaecoo-be/app/module/articles"
|
||||
"jaecoo-be/app/module/banners"
|
||||
"jaecoo-be/app/module/cities"
|
||||
"jaecoo-be/app/module/client_approval_settings"
|
||||
"jaecoo-be/app/module/clients"
|
||||
"jaecoo-be/app/module/custom_static_pages"
|
||||
"jaecoo-be/app/module/districts"
|
||||
"jaecoo-be/app/module/feedbacks"
|
||||
|
|
@ -70,6 +72,8 @@ func main() {
|
|||
article_comments.NewArticleCommentsModule,
|
||||
banners.NewBannersModule,
|
||||
cities.NewCitiesModule,
|
||||
client_approval_settings.NewClientApprovalSettingsModule,
|
||||
clients.NewClientsModule,
|
||||
custom_static_pages.NewCustomStaticPagesModule,
|
||||
districts.NewDistrictsModule,
|
||||
feedbacks.NewFeedbacksModule,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,818 @@
|
|||
# API Documentation - Sistem Approval Artikel Dinamis
|
||||
|
||||
## Overview
|
||||
Dokumentasi ini menjelaskan API endpoints yang tersedia untuk sistem approval artikel dinamis. Sistem ini memungkinkan pembuatan workflow approval yang fleksibel dengan multiple level dan proses revisi.
|
||||
|
||||
## Base URL
|
||||
```
|
||||
http://localhost:8800/api
|
||||
```
|
||||
|
||||
## Authentication
|
||||
Semua endpoint memerlukan Bearer token authentication:
|
||||
```
|
||||
Authorization: Bearer <your_jwt_token>
|
||||
```
|
||||
|
||||
## 1. Approval Workflows Management
|
||||
|
||||
### 1.1 Get All Workflows
|
||||
```http
|
||||
GET /approval-workflows
|
||||
```
|
||||
|
||||
**Query Parameters:**
|
||||
- `page` (optional): Page number (default: 1)
|
||||
- `limit` (optional): Items per page (default: 10)
|
||||
- `search` (optional): Search by workflow name
|
||||
- `is_active` (optional): Filter by active status (true/false)
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"messages": ["Workflows retrieved successfully"],
|
||||
"data": [
|
||||
{
|
||||
"id": 1,
|
||||
"name": "Standard 3-Level Approval",
|
||||
"description": "Default workflow with 3 approval levels",
|
||||
"is_active": true,
|
||||
"created_at": "2024-01-15T10:30:00Z",
|
||||
"updated_at": "2024-01-15T10:30:00Z",
|
||||
"steps": [
|
||||
{
|
||||
"id": 1,
|
||||
"step_order": 1,
|
||||
"user_level_id": 3,
|
||||
"user_level": {
|
||||
"id": 3,
|
||||
"level_name": "Approver Level 3",
|
||||
"level_number": 3
|
||||
},
|
||||
"is_required": true,
|
||||
"can_skip": false
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"pagination": {
|
||||
"current_page": 1,
|
||||
"per_page": 10,
|
||||
"total": 5,
|
||||
"total_pages": 1
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 1.2 Get Workflow by ID
|
||||
```http
|
||||
GET /approval-workflows/{id}
|
||||
```
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"messages": ["Workflow retrieved successfully"],
|
||||
"data": {
|
||||
"id": 1,
|
||||
"name": "Standard 3-Level Approval",
|
||||
"description": "Default workflow with 3 approval levels",
|
||||
"is_active": true,
|
||||
"steps": [
|
||||
{
|
||||
"id": 1,
|
||||
"step_order": 1,
|
||||
"user_level_id": 3,
|
||||
"is_required": true,
|
||||
"can_skip": false
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 1.3 Create New Workflow
|
||||
```http
|
||||
POST /approval-workflows
|
||||
```
|
||||
|
||||
**Request Body:**
|
||||
```json
|
||||
{
|
||||
"name": "Custom 2-Level Approval",
|
||||
"description": "Fast track approval for urgent articles",
|
||||
"steps": [
|
||||
{
|
||||
"stepOrder": 1,
|
||||
"userLevelId": 2,
|
||||
"isRequired": true,
|
||||
"canSkip": false
|
||||
},
|
||||
{
|
||||
"stepOrder": 2,
|
||||
"userLevelId": 1,
|
||||
"isRequired": true,
|
||||
"canSkip": false
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**Validation Rules:**
|
||||
- `name`: Required, max 255 characters
|
||||
- `steps`: Required, minimum 1 step
|
||||
- `stepOrder`: Must be sequential starting from 1
|
||||
- `userLevelId`: Must exist in user_levels table
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"messages": ["Workflow created successfully"],
|
||||
"data": {
|
||||
"id": 5,
|
||||
"name": "Custom 2-Level Approval",
|
||||
"description": "Fast track approval for urgent articles",
|
||||
"is_active": true,
|
||||
"created_at": "2024-01-15T14:30:00Z"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 1.4 Update Workflow
|
||||
```http
|
||||
PUT /approval-workflows/{id}
|
||||
```
|
||||
|
||||
**Request Body:**
|
||||
```json
|
||||
{
|
||||
"name": "Updated Workflow Name",
|
||||
"description": "Updated description",
|
||||
"is_active": true
|
||||
}
|
||||
```
|
||||
|
||||
### 1.5 Delete Workflow
|
||||
```http
|
||||
DELETE /approval-workflows/{id}
|
||||
```
|
||||
|
||||
**Note:** Workflow can only be deleted if no articles are currently using it.
|
||||
|
||||
## 2. Article Approval Management
|
||||
|
||||
### 2.1 Submit Article for Approval
|
||||
```http
|
||||
POST /articles/{id}/submit-approval
|
||||
```
|
||||
|
||||
**Request Body:**
|
||||
```json
|
||||
{
|
||||
"workflowId": 2
|
||||
}
|
||||
```
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"messages": ["Article submitted for approval successfully"],
|
||||
"data": {
|
||||
"id": 15,
|
||||
"article_id": 123,
|
||||
"workflow_id": 2,
|
||||
"current_step": 1,
|
||||
"status_id": 1,
|
||||
"created_at": "2024-01-15T15:00:00Z"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2.2 Approve Current Step
|
||||
```http
|
||||
POST /articles/{id}/approve
|
||||
```
|
||||
|
||||
**Request Body:**
|
||||
```json
|
||||
{
|
||||
"message": "Article content is excellent, approved for next level"
|
||||
}
|
||||
```
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"messages": ["Article approved successfully"],
|
||||
"data": {
|
||||
"next_step": 2,
|
||||
"status": "moved_to_next_level"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2.3 Reject Article
|
||||
```http
|
||||
POST /articles/{id}/reject
|
||||
```
|
||||
|
||||
**Request Body:**
|
||||
```json
|
||||
{
|
||||
"message": "Article does not meet quality standards"
|
||||
}
|
||||
```
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"messages": ["Article rejected successfully"]
|
||||
}
|
||||
```
|
||||
|
||||
### 2.4 Request Revision
|
||||
```http
|
||||
POST /articles/{id}/request-revision
|
||||
```
|
||||
|
||||
**Request Body:**
|
||||
```json
|
||||
{
|
||||
"message": "Please fix grammar issues in paragraph 3 and add more references"
|
||||
}
|
||||
```
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"messages": ["Revision requested successfully"]
|
||||
}
|
||||
```
|
||||
|
||||
### 2.5 Resubmit After Revision
|
||||
```http
|
||||
POST /articles/{id}/resubmit
|
||||
```
|
||||
|
||||
**Request Body:**
|
||||
```json
|
||||
{
|
||||
"message": "Fixed all requested issues"
|
||||
}
|
||||
```
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"messages": ["Article resubmitted successfully"],
|
||||
"data": {
|
||||
"current_step": 1,
|
||||
"status": "back_in_approval_flow"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 3. Approval Dashboard
|
||||
|
||||
### 3.1 Get Pending Approvals for Current User
|
||||
```http
|
||||
GET /approvals/pending
|
||||
```
|
||||
|
||||
**Query Parameters:**
|
||||
- `page` (optional): Page number (default: 1)
|
||||
- `limit` (optional): Items per page (default: 10, max: 50)
|
||||
- `priority` (optional): Filter by priority (high, medium, low)
|
||||
- `category_id` (optional): Filter by article category
|
||||
- `search` (optional): Search in article title or author name
|
||||
- `date_from` (optional): Filter articles submitted from date (YYYY-MM-DD)
|
||||
- `date_to` (optional): Filter articles submitted to date (YYYY-MM-DD)
|
||||
- `sort_by` (optional): Sort by field (waiting_time, created_at, title, priority)
|
||||
- `sort_order` (optional): Sort order (asc, desc) - default: desc
|
||||
- `workflow_id` (optional): Filter by specific workflow
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"messages": ["Pending approvals retrieved successfully"],
|
||||
"data": [
|
||||
{
|
||||
"id": 15,
|
||||
"article": {
|
||||
"id": 123,
|
||||
"title": "Understanding Machine Learning",
|
||||
"excerpt": "This article explores the fundamentals of machine learning...",
|
||||
"author": {
|
||||
"id": 45,
|
||||
"name": "John Doe",
|
||||
"email": "john@example.com",
|
||||
"profile_picture": "https://example.com/avatar.jpg"
|
||||
},
|
||||
"category": {
|
||||
"id": 5,
|
||||
"name": "Technology",
|
||||
"color": "#3B82F6"
|
||||
},
|
||||
"word_count": 1250,
|
||||
"estimated_read_time": "5 min",
|
||||
"created_at": "2024-01-15T10:00:00Z",
|
||||
"submitted_at": "2024-01-15T14:30:00Z"
|
||||
},
|
||||
"workflow": {
|
||||
"id": 2,
|
||||
"name": "Standard 3-Level Approval",
|
||||
"total_steps": 3
|
||||
},
|
||||
"current_step": 2,
|
||||
"my_step_order": 2,
|
||||
"waiting_since": "2024-01-15T14:30:00Z",
|
||||
"waiting_duration_hours": 6.5,
|
||||
"priority": "medium",
|
||||
"previous_approvals": [
|
||||
{
|
||||
"step_order": 1,
|
||||
"approver_name": "Jane Smith",
|
||||
"approved_at": "2024-01-15T14:30:00Z",
|
||||
"message": "Content looks good"
|
||||
}
|
||||
],
|
||||
"urgency_score": 7.5,
|
||||
"can_approve": true,
|
||||
"can_reject": true,
|
||||
"can_request_revision": true
|
||||
}
|
||||
],
|
||||
"pagination": {
|
||||
"current_page": 1,
|
||||
"per_page": 10,
|
||||
"total": 5,
|
||||
"total_pages": 1
|
||||
},
|
||||
"summary": {
|
||||
"total_pending": 5,
|
||||
"high_priority": 2,
|
||||
"medium_priority": 2,
|
||||
"low_priority": 1,
|
||||
"overdue_count": 1,
|
||||
"avg_waiting_hours": 8.2
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3.2 Get Approval History for Article
|
||||
```http
|
||||
GET /articles/{id}/approval-history
|
||||
```
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"messages": ["Approval history retrieved successfully"],
|
||||
"data": {
|
||||
"article_id": 123,
|
||||
"workflow": {
|
||||
"id": 2,
|
||||
"name": "Standard 3-Level Approval"
|
||||
},
|
||||
"current_step": 2,
|
||||
"status": "in_progress",
|
||||
"history": [
|
||||
{
|
||||
"id": 1,
|
||||
"step_order": 1,
|
||||
"action": "approved",
|
||||
"message": "Good content, approved",
|
||||
"approver": {
|
||||
"id": 67,
|
||||
"name": "Jane Smith",
|
||||
"level": "Approver Level 3"
|
||||
},
|
||||
"approved_at": "2024-01-15T14:30:00Z"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3.3 Get My Approval Statistics
|
||||
```http
|
||||
GET /approvals/my-stats
|
||||
```
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"messages": ["Statistics retrieved successfully"],
|
||||
"data": {
|
||||
"pending_count": 5,
|
||||
"overdue_count": 1,
|
||||
"approved_today": 3,
|
||||
"approved_this_week": 15,
|
||||
"approved_this_month": 45,
|
||||
"rejected_this_month": 2,
|
||||
"revision_requests_this_month": 8,
|
||||
"average_approval_time_hours": 4.5,
|
||||
"fastest_approval_minutes": 15,
|
||||
"slowest_approval_hours": 48,
|
||||
"approval_rate_percentage": 85.2,
|
||||
"categories_breakdown": [
|
||||
{
|
||||
"category_name": "Technology",
|
||||
"pending": 2,
|
||||
"approved_this_month": 12
|
||||
},
|
||||
{
|
||||
"category_name": "Health",
|
||||
"pending": 3,
|
||||
"approved_this_month": 8
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3.4 Get Articles Requiring My Approval (Detailed View)
|
||||
```http
|
||||
GET /approvals/my-queue
|
||||
```
|
||||
|
||||
**Query Parameters:**
|
||||
- Same as `/approvals/pending` but specifically filtered for current user's level
|
||||
- `include_preview` (optional): Include article content preview (true/false)
|
||||
- `urgency_only` (optional): Show only urgent articles (true/false)
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"messages": ["My approval queue retrieved successfully"],
|
||||
"data": [
|
||||
{
|
||||
"id": 15,
|
||||
"article": {
|
||||
"id": 123,
|
||||
"title": "Understanding Machine Learning",
|
||||
"content_preview": "Machine learning is a subset of artificial intelligence that enables computers to learn and improve from experience without being explicitly programmed...",
|
||||
"full_content_available": true,
|
||||
"author": {
|
||||
"id": 45,
|
||||
"name": "John Doe",
|
||||
"reputation_score": 8.5,
|
||||
"articles_published": 25,
|
||||
"approval_success_rate": 92.3
|
||||
},
|
||||
"submission_notes": "This is an updated version based on previous feedback",
|
||||
"tags": ["AI", "Technology", "Education"],
|
||||
"seo_score": 85,
|
||||
"readability_score": "Good"
|
||||
},
|
||||
"approval_context": {
|
||||
"my_role_in_workflow": "Senior Editor Review",
|
||||
"step_description": "Review content quality and technical accuracy",
|
||||
"expected_action": "Approve or request technical revisions",
|
||||
"deadline": "2024-01-16T18:00:00Z",
|
||||
"is_overdue": false,
|
||||
"escalation_available": true
|
||||
},
|
||||
"workflow_progress": {
|
||||
"completed_steps": 1,
|
||||
"total_steps": 3,
|
||||
"progress_percentage": 33.3,
|
||||
"next_approver": "Chief Editor"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### 3.5 Get Approval Workload Distribution
|
||||
```http
|
||||
GET /approvals/workload
|
||||
```
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"messages": ["Workload distribution retrieved successfully"],
|
||||
"data": {
|
||||
"my_level": {
|
||||
"level_name": "Senior Editor",
|
||||
"level_number": 2,
|
||||
"pending_articles": 5,
|
||||
"avg_daily_approvals": 8.5
|
||||
},
|
||||
"team_comparison": [
|
||||
{
|
||||
"approver_name": "Jane Smith",
|
||||
"level_name": "Senior Editor",
|
||||
"pending_articles": 3,
|
||||
"avg_response_time_hours": 2.5
|
||||
},
|
||||
{
|
||||
"approver_name": "Mike Johnson",
|
||||
"level_name": "Senior Editor",
|
||||
"pending_articles": 7,
|
||||
"avg_response_time_hours": 4.2
|
||||
}
|
||||
],
|
||||
"bottlenecks": [
|
||||
{
|
||||
"level_name": "Chief Editor",
|
||||
"pending_count": 12,
|
||||
"avg_waiting_time_hours": 18.5,
|
||||
"is_bottleneck": true
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3.6 Bulk Approval Actions
|
||||
```http
|
||||
POST /approvals/bulk-action
|
||||
```
|
||||
|
||||
**Request Body:**
|
||||
```json
|
||||
{
|
||||
"article_ids": [123, 124, 125],
|
||||
"action": "approve",
|
||||
"message": "Bulk approval for similar content type",
|
||||
"apply_to_similar": false
|
||||
}
|
||||
```
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"messages": ["Bulk action completed successfully"],
|
||||
"data": {
|
||||
"processed_count": 3,
|
||||
"successful_count": 3,
|
||||
"failed_count": 0,
|
||||
"results": [
|
||||
{
|
||||
"article_id": 123,
|
||||
"status": "success",
|
||||
"next_step": 3
|
||||
},
|
||||
{
|
||||
"article_id": 124,
|
||||
"status": "success",
|
||||
"next_step": 3
|
||||
},
|
||||
{
|
||||
"article_id": 125,
|
||||
"status": "success",
|
||||
"next_step": "published"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 4. User Level Management
|
||||
|
||||
### 4.1 Get Available User Levels
|
||||
```http
|
||||
GET /user-levels
|
||||
```
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"messages": ["User levels retrieved successfully"],
|
||||
"data": [
|
||||
{
|
||||
"id": 1,
|
||||
"level_name": "Chief Editor",
|
||||
"level_number": 1,
|
||||
"parent_level_id": null,
|
||||
"is_approval_active": true
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"level_name": "Senior Editor",
|
||||
"level_number": 2,
|
||||
"parent_level_id": 1,
|
||||
"is_approval_active": true
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"level_name": "Junior Editor",
|
||||
"level_number": 3,
|
||||
"parent_level_id": 2,
|
||||
"is_approval_active": true
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## 5. Error Responses
|
||||
|
||||
### 5.1 Validation Error (400)
|
||||
```json
|
||||
{
|
||||
"success": false,
|
||||
"messages": ["Validation failed"],
|
||||
"errors": {
|
||||
"name": ["Name is required"],
|
||||
"steps": ["At least one step is required"]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 5.2 Unauthorized (401)
|
||||
```json
|
||||
{
|
||||
"success": false,
|
||||
"messages": ["Unauthorized access"]
|
||||
}
|
||||
```
|
||||
|
||||
### 5.3 Forbidden (403)
|
||||
```json
|
||||
{
|
||||
"success": false,
|
||||
"messages": ["You don't have permission to approve this article"]
|
||||
}
|
||||
```
|
||||
|
||||
### 5.4 Not Found (404)
|
||||
```json
|
||||
{
|
||||
"success": false,
|
||||
"messages": ["Article not found"]
|
||||
}
|
||||
```
|
||||
|
||||
### 5.5 Business Logic Error (422)
|
||||
```json
|
||||
{
|
||||
"success": false,
|
||||
"messages": ["Article already has active approval flow"]
|
||||
}
|
||||
```
|
||||
|
||||
### 5.6 Internal Server Error (500)
|
||||
```json
|
||||
{
|
||||
"success": false,
|
||||
"messages": ["Internal server error occurred"]
|
||||
}
|
||||
```
|
||||
|
||||
## 6. Webhook Events (Optional)
|
||||
|
||||
Sistem dapat mengirim webhook notifications untuk event-event penting:
|
||||
|
||||
### 6.1 Article Submitted for Approval
|
||||
```json
|
||||
{
|
||||
"event": "article.submitted_for_approval",
|
||||
"timestamp": "2024-01-15T15:00:00Z",
|
||||
"data": {
|
||||
"article_id": 123,
|
||||
"workflow_id": 2,
|
||||
"submitted_by": {
|
||||
"id": 45,
|
||||
"name": "John Doe"
|
||||
},
|
||||
"next_approver_level": 3
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 6.2 Article Approved
|
||||
```json
|
||||
{
|
||||
"event": "article.approved",
|
||||
"timestamp": "2024-01-15T16:00:00Z",
|
||||
"data": {
|
||||
"article_id": 123,
|
||||
"approved_by": {
|
||||
"id": 67,
|
||||
"name": "Jane Smith",
|
||||
"level": 3
|
||||
},
|
||||
"current_step": 2,
|
||||
"is_final_approval": false
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 6.3 Article Published
|
||||
```json
|
||||
{
|
||||
"event": "article.published",
|
||||
"timestamp": "2024-01-15T17:00:00Z",
|
||||
"data": {
|
||||
"article_id": 123,
|
||||
"final_approver": {
|
||||
"id": 89,
|
||||
"name": "Chief Editor",
|
||||
"level": 1
|
||||
},
|
||||
"total_approval_time_hours": 6.5
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 6.4 Revision Requested
|
||||
```json
|
||||
{
|
||||
"event": "article.revision_requested",
|
||||
"timestamp": "2024-01-15T16:30:00Z",
|
||||
"data": {
|
||||
"article_id": 123,
|
||||
"requested_by": {
|
||||
"id": 67,
|
||||
"name": "Jane Smith",
|
||||
"level": 2
|
||||
},
|
||||
"message": "Please fix grammar issues",
|
||||
"author": {
|
||||
"id": 45,
|
||||
"name": "John Doe"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 7. Rate Limiting
|
||||
|
||||
API menggunakan rate limiting untuk mencegah abuse:
|
||||
- **General endpoints**: 100 requests per minute per user
|
||||
- **Approval actions**: 50 requests per minute per user
|
||||
- **Workflow management**: 20 requests per minute per user
|
||||
|
||||
## 8. Pagination
|
||||
|
||||
Semua endpoint yang mengembalikan list menggunakan pagination:
|
||||
- Default `limit`: 10
|
||||
- Maximum `limit`: 100
|
||||
- Default `page`: 1
|
||||
|
||||
## 9. Filtering dan Sorting
|
||||
|
||||
Beberapa endpoint mendukung filtering dan sorting:
|
||||
|
||||
### Query Parameters untuk Filtering:
|
||||
- `status`: Filter by status
|
||||
- `user_level`: Filter by user level
|
||||
- `date_from`: Filter from date (YYYY-MM-DD)
|
||||
- `date_to`: Filter to date (YYYY-MM-DD)
|
||||
- `search`: Search in title/content
|
||||
|
||||
### Query Parameters untuk Sorting:
|
||||
- `sort_by`: Field to sort by (created_at, updated_at, title, etc.)
|
||||
- `sort_order`: asc atau desc (default: desc)
|
||||
|
||||
Contoh:
|
||||
```
|
||||
GET /articles?status=pending&sort_by=created_at&sort_order=asc&page=1&limit=20
|
||||
```
|
||||
|
||||
## 10. Bulk Operations
|
||||
|
||||
### 10.1 Bulk Approve Articles
|
||||
```http
|
||||
POST /articles/bulk-approve
|
||||
```
|
||||
|
||||
**Request Body:**
|
||||
```json
|
||||
{
|
||||
"article_ids": [123, 124, 125],
|
||||
"message": "Bulk approval for similar articles"
|
||||
}
|
||||
```
|
||||
|
||||
### 10.2 Bulk Assign Workflow
|
||||
```http
|
||||
POST /articles/bulk-assign-workflow
|
||||
```
|
||||
|
||||
**Request Body:**
|
||||
```json
|
||||
{
|
||||
"article_ids": [123, 124, 125],
|
||||
"workflow_id": 2
|
||||
}
|
||||
```
|
||||
|
||||
Dokumentasi API ini memberikan panduan lengkap untuk mengintegrasikan sistem approval artikel dinamis dengan frontend atau sistem eksternal lainnya.
|
||||
|
|
@ -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.
|
||||
|
|
@ -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.
|
||||
|
|
@ -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.
|
||||
|
|
@ -0,0 +1,335 @@
|
|||
# Plan Implementasi Sistem Approval Artikel Dinamis
|
||||
|
||||
## 1. Analisis Sistem Saat Ini
|
||||
|
||||
### Struktur Database yang Sudah Ada:
|
||||
- **Articles**: Memiliki field `NeedApprovalFrom`, `HasApprovedBy`, `StatusId` untuk tracking approval
|
||||
- **ArticleApprovals**: Menyimpan history approval dengan `ApprovalAtLevel`
|
||||
- **Users**: Terhubung dengan `UserLevels` dan `UserRoles`
|
||||
- **UserLevels**: Memiliki `LevelNumber`, `ParentLevelId`, `IsApprovalActive`
|
||||
- **UserRoles**: Terhubung dengan `UserLevels` melalui `UserRoleLevelDetails`
|
||||
- **UserRoleAccesses**: Memiliki `IsApprovalEnabled` untuk kontrol akses approval
|
||||
- **MasterApprovalStatuses**: Status approval (pending, approved, rejected)
|
||||
|
||||
### Logika Approval Saat Ini:
|
||||
- Hard-coded untuk 3 level (level 1, 2, 3)
|
||||
- Approval naik ke parent level jika disetujui
|
||||
- Kembali ke contributor jika ditolak
|
||||
- Status: 1=Pending, 2=Approved, 3=Rejected
|
||||
|
||||
## 2. Kebutuhan Sistem Baru
|
||||
|
||||
### Functional Requirements:
|
||||
1. **Dynamic Approval Levels**: Sistem harus mendukung 1-N level approval
|
||||
2. **Flexible Workflow**: Approval flow bisa diubah sewaktu-waktu
|
||||
3. **Role-based Access**: Approver dan Contributor dengan hak akses berbeda
|
||||
4. **Revision Handling**: Artikel kembali ke contributor saat revisi diminta
|
||||
5. **Audit Trail**: Tracking lengkap history approval
|
||||
|
||||
### User Stories:
|
||||
- Sebagai **Contributor**: Saya bisa membuat artikel dan submit untuk approval
|
||||
- Sebagai **Approver Level N**: Saya bisa approve/reject/request revision artikel
|
||||
- Sebagai **Admin**: Saya bisa mengatur approval workflow secara dinamis
|
||||
|
||||
## 3. Desain Database Baru
|
||||
|
||||
### 3.1 Tabel Baru yang Diperlukan:
|
||||
|
||||
#### `approval_workflows`
|
||||
```sql
|
||||
CREATE TABLE approval_workflows (
|
||||
id SERIAL PRIMARY KEY,
|
||||
name VARCHAR(255) NOT NULL,
|
||||
description TEXT,
|
||||
is_active BOOLEAN DEFAULT true,
|
||||
client_id UUID,
|
||||
created_at TIMESTAMP DEFAULT NOW(),
|
||||
updated_at TIMESTAMP DEFAULT NOW()
|
||||
);
|
||||
```
|
||||
|
||||
#### `approval_workflow_steps`
|
||||
```sql
|
||||
CREATE TABLE approval_workflow_steps (
|
||||
id SERIAL PRIMARY KEY,
|
||||
workflow_id INT REFERENCES approval_workflows(id),
|
||||
step_order INT NOT NULL,
|
||||
user_level_id INT REFERENCES user_levels(id),
|
||||
is_required BOOLEAN DEFAULT true,
|
||||
can_skip BOOLEAN DEFAULT false,
|
||||
client_id UUID,
|
||||
created_at TIMESTAMP DEFAULT NOW(),
|
||||
updated_at TIMESTAMP DEFAULT NOW()
|
||||
);
|
||||
```
|
||||
|
||||
#### `article_approval_flows`
|
||||
```sql
|
||||
CREATE TABLE article_approval_flows (
|
||||
id SERIAL PRIMARY KEY,
|
||||
article_id INT REFERENCES articles(id),
|
||||
workflow_id INT REFERENCES approval_workflows(id),
|
||||
current_step INT DEFAULT 1,
|
||||
status_id INT DEFAULT 1, -- 1=In Progress, 2=Completed, 3=Rejected
|
||||
client_id UUID,
|
||||
created_at TIMESTAMP DEFAULT NOW(),
|
||||
updated_at TIMESTAMP DEFAULT NOW()
|
||||
);
|
||||
```
|
||||
|
||||
#### `article_approval_step_logs`
|
||||
```sql
|
||||
CREATE TABLE article_approval_step_logs (
|
||||
id SERIAL PRIMARY KEY,
|
||||
article_flow_id INT REFERENCES article_approval_flows(id),
|
||||
step_order INT NOT NULL,
|
||||
user_level_id INT REFERENCES user_levels(id),
|
||||
approved_by INT REFERENCES users(id),
|
||||
action VARCHAR(50) NOT NULL, -- 'approved', 'rejected', 'revision_requested'
|
||||
message TEXT,
|
||||
approved_at TIMESTAMP,
|
||||
client_id UUID,
|
||||
created_at TIMESTAMP DEFAULT NOW()
|
||||
);
|
||||
```
|
||||
|
||||
### 3.2 Modifikasi Tabel Existing:
|
||||
|
||||
#### `articles` - Tambah field:
|
||||
```sql
|
||||
ALTER TABLE articles ADD COLUMN workflow_id INT REFERENCES approval_workflows(id);
|
||||
ALTER TABLE articles ADD COLUMN current_approval_step INT DEFAULT 0;
|
||||
-- Keep existing fields: need_approval_from, has_approved_by, status_id untuk backward compatibility
|
||||
```
|
||||
|
||||
#### `user_roles` - Tambah field:
|
||||
```sql
|
||||
ALTER TABLE user_roles ADD COLUMN role_type VARCHAR(50) DEFAULT 'contributor'; -- 'contributor', 'approver', 'admin'
|
||||
```
|
||||
|
||||
## 4. Implementasi Backend
|
||||
|
||||
### 4.1 Entities Baru:
|
||||
- `ApprovalWorkflows`
|
||||
- `ApprovalWorkflowSteps`
|
||||
- `ArticleApprovalFlows`
|
||||
- `ArticleApprovalStepLogs`
|
||||
|
||||
### 4.2 Modules Baru:
|
||||
|
||||
#### `approval_workflows`
|
||||
- **Repository**: CRUD operations untuk workflow management
|
||||
- **Service**: Business logic untuk workflow creation/modification
|
||||
- **Controller**: API endpoints untuk workflow management
|
||||
- **Request/Response**: DTOs untuk workflow operations
|
||||
|
||||
#### `article_approval_flows`
|
||||
- **Repository**: Tracking approval progress per artikel
|
||||
- **Service**: Core approval logic, step progression
|
||||
- **Controller**: API untuk approval actions
|
||||
- **Request/Response**: DTOs untuk approval operations
|
||||
|
||||
### 4.3 Modifikasi Modules Existing:
|
||||
|
||||
#### `articles/service`
|
||||
- Refactor `UpdateApproval()` method untuk menggunakan dynamic workflow
|
||||
- Tambah method `InitiateApprovalFlow()` untuk memulai approval process
|
||||
- Tambah method `ProcessApprovalStep()` untuk handle approval actions
|
||||
|
||||
#### `article_approvals`
|
||||
- Extend untuk support dynamic workflow
|
||||
- Integrate dengan `ArticleApprovalStepLogs`
|
||||
|
||||
## 5. API Design
|
||||
|
||||
### 5.1 Workflow Management APIs:
|
||||
```
|
||||
GET /api/approval-workflows # List all workflows
|
||||
POST /api/approval-workflows # Create new workflow
|
||||
GET /api/approval-workflows/{id} # Get workflow details
|
||||
PUT /api/approval-workflows/{id} # Update workflow
|
||||
DELETE /api/approval-workflows/{id} # Delete workflow
|
||||
|
||||
GET /api/approval-workflows/{id}/steps # Get workflow steps
|
||||
POST /api/approval-workflows/{id}/steps # Add workflow step
|
||||
PUT /api/approval-workflow-steps/{id} # Update workflow step
|
||||
DELETE /api/approval-workflow-steps/{id} # Delete workflow step
|
||||
```
|
||||
|
||||
### 5.2 Article Approval APIs:
|
||||
```
|
||||
POST /api/articles/{id}/submit-approval # Submit article for approval
|
||||
POST /api/articles/{id}/approve # Approve current step
|
||||
POST /api/articles/{id}/reject # Reject article
|
||||
POST /api/articles/{id}/request-revision # Request revision
|
||||
GET /api/articles/{id}/approval-history # Get approval history
|
||||
GET /api/articles/pending-approval # Get articles pending approval for current user
|
||||
```
|
||||
|
||||
### 5.3 Request/Response Models:
|
||||
|
||||
#### Workflow Creation:
|
||||
```json
|
||||
{
|
||||
"name": "Standard Article Approval",
|
||||
"description": "3-level approval process",
|
||||
"steps": [
|
||||
{
|
||||
"stepOrder": 1,
|
||||
"userLevelId": 3,
|
||||
"isRequired": true,
|
||||
"canSkip": false
|
||||
},
|
||||
{
|
||||
"stepOrder": 2,
|
||||
"userLevelId": 2,
|
||||
"isRequired": true,
|
||||
"canSkip": false
|
||||
},
|
||||
{
|
||||
"stepOrder": 3,
|
||||
"userLevelId": 1,
|
||||
"isRequired": true,
|
||||
"canSkip": false
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
#### Approval Action:
|
||||
```json
|
||||
{
|
||||
"action": "approved", // "approved", "rejected", "revision_requested"
|
||||
"message": "Article looks good, approved for next level"
|
||||
}
|
||||
```
|
||||
|
||||
## 6. Business Logic Flow
|
||||
|
||||
### 6.1 Article Submission Flow:
|
||||
1. Contributor creates article (status: draft)
|
||||
2. Contributor submits for approval
|
||||
3. System creates `ArticleApprovalFlow` record
|
||||
4. System sets article status to "pending approval"
|
||||
5. System notifies first level approver
|
||||
|
||||
### 6.2 Approval Process Flow:
|
||||
1. Approver receives notification
|
||||
2. Approver reviews article
|
||||
3. Approver takes action:
|
||||
- **Approve**: Move to next step or publish if final step
|
||||
- **Reject**: Set article status to rejected, end flow
|
||||
- **Request Revision**: Send back to contributor
|
||||
|
||||
### 6.3 Revision Flow:
|
||||
1. Article returns to contributor
|
||||
2. Contributor makes revisions
|
||||
3. Contributor resubmits
|
||||
4. Approval flow restarts from step 1
|
||||
|
||||
## 7. Implementation Phases
|
||||
|
||||
### Phase 1: Database & Core Entities
|
||||
- [ ] Create new database tables
|
||||
- [ ] Implement new entities
|
||||
- [ ] Create migration scripts
|
||||
- [ ] Update existing entities
|
||||
|
||||
### Phase 2: Workflow Management
|
||||
- [ ] Implement `approval_workflows` module
|
||||
- [ ] Create workflow CRUD operations
|
||||
- [ ] Implement workflow step management
|
||||
- [ ] Create admin interface for workflow setup
|
||||
|
||||
### Phase 3: Dynamic Approval Engine
|
||||
- [ ] Implement `article_approval_flows` module
|
||||
- [ ] Refactor articles service for dynamic approval
|
||||
- [ ] Create approval processing logic
|
||||
- [ ] Implement notification system
|
||||
|
||||
### Phase 4: API & Integration
|
||||
- [ ] Create approval APIs
|
||||
- [ ] Update existing article APIs
|
||||
- [ ] Implement role-based access control
|
||||
- [ ] Create approval dashboard
|
||||
|
||||
### Phase 5: Testing & Migration
|
||||
- [ ] Unit tests for all new modules
|
||||
- [ ] Integration tests for approval flows
|
||||
- [ ] Data migration from old system
|
||||
- [ ] Performance testing
|
||||
|
||||
## 8. Backward Compatibility
|
||||
|
||||
### Migration Strategy:
|
||||
1. **Dual System**: Run old and new system parallel
|
||||
2. **Default Workflow**: Create default workflow matching current 3-level system
|
||||
3. **Gradual Migration**: Migrate existing articles to new system
|
||||
4. **Fallback**: Keep old approval logic as fallback
|
||||
|
||||
### Data Migration:
|
||||
```sql
|
||||
-- Create default workflow
|
||||
INSERT INTO approval_workflows (name, description)
|
||||
VALUES ('Legacy 3-Level Approval', 'Default 3-level approval system');
|
||||
|
||||
-- Create workflow steps
|
||||
INSERT INTO approval_workflow_steps (workflow_id, step_order, user_level_id)
|
||||
VALUES
|
||||
(1, 1, 3), -- Level 3 approver
|
||||
(1, 2, 2), -- Level 2 approver
|
||||
(1, 3, 1); -- Level 1 approver
|
||||
|
||||
-- Update existing articles
|
||||
UPDATE articles SET workflow_id = 1 WHERE workflow_id IS NULL;
|
||||
```
|
||||
|
||||
## 9. Security Considerations
|
||||
|
||||
### Access Control:
|
||||
- **Contributor**: Can only create and edit own articles
|
||||
- **Approver**: Can only approve articles at their assigned level
|
||||
- **Admin**: Can manage workflows and override approvals
|
||||
|
||||
### Validation:
|
||||
- Validate user has permission for approval level
|
||||
- Prevent approval of own articles
|
||||
- Validate workflow step sequence
|
||||
- Audit all approval actions
|
||||
|
||||
## 10. Performance Considerations
|
||||
|
||||
### Optimization:
|
||||
- Index on `article_approval_flows.article_id`
|
||||
- Index on `article_approval_flows.workflow_id`
|
||||
- Index on `article_approval_step_logs.article_flow_id`
|
||||
- Cache active workflows
|
||||
- Batch notification processing
|
||||
|
||||
### Monitoring:
|
||||
- Track approval processing time
|
||||
- Monitor workflow bottlenecks
|
||||
- Alert on stuck approvals
|
||||
- Dashboard for approval metrics
|
||||
|
||||
## 11. Future Enhancements
|
||||
|
||||
### Possible Extensions:
|
||||
1. **Conditional Workflows**: Different workflows based on article type/category
|
||||
2. **Parallel Approvals**: Multiple approvers at same level
|
||||
3. **Time-based Escalation**: Auto-escalate if approval takes too long
|
||||
4. **External Integrations**: Email/Slack notifications
|
||||
5. **Approval Templates**: Pre-defined approval messages
|
||||
6. **Bulk Approvals**: Approve multiple articles at once
|
||||
|
||||
---
|
||||
|
||||
**Estimasi Waktu Implementasi**: 4-6 minggu
|
||||
**Kompleksitas**: Medium-High
|
||||
**Risk Level**: Medium (karena perubahan core business logic)
|
||||
|
||||
**Next Steps**:
|
||||
1. Review dan approval plan ini
|
||||
2. Setup development environment
|
||||
3. Mulai implementasi Phase 1
|
||||
4. Regular progress review setiap minggu
|
||||
|
|
@ -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.*
|
||||
File diff suppressed because it is too large
Load Diff
|
|
@ -101,3 +101,12 @@ func Unauthorized() *Response {
|
|||
},
|
||||
}
|
||||
}
|
||||
|
||||
func ErrorBadRequest(c *fiber.Ctx, message string) error {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(Response{
|
||||
Success: false,
|
||||
Code: 400,
|
||||
Messages: Messages{message},
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue