This commit is contained in:
Anang Yusman 2026-01-08 15:04:21 +08:00
parent 62149477b6
commit b2dc684ade
40 changed files with 38518 additions and 1 deletions

View File

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

View File

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

View File

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

View File

@ -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()"`
}

View File

@ -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()"`
}

View File

@ -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()"`
}

View File

@ -95,6 +95,8 @@ func Models() []interface{} {
entity.ArticleComments{},
entity.AuditTrails{},
entity.Banners{},
entity.Clients{},
entity.ClientApprovalSettings{},
entity.Cities{},
entity.CsrfTokenRecords{},
entity.CustomStaticPages{},

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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()

View File

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

12213
docs/docs.go Normal file

File diff suppressed because it is too large Load Diff

12184
docs/swagger.json Normal file

File diff suppressed because it is too large Load Diff

7823
docs/swagger.yaml Normal file

File diff suppressed because it is too large Load Diff

View File

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

818
plan/api-documentation.md Normal file
View File

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

View File

@ -0,0 +1,347 @@
# Approval Workflow Architecture & Module Relationships
## 📋 **Overview**
Sistem approval workflow di MEDOLS menggunakan arsitektur multi-module yang saling terintegrasi untuk mengelola proses persetujuan artikel secara dinamis. Setiap module memiliki peran spesifik dalam alur approval yang kompleks.
## 🏗️ **Module Architecture**
### **1. Core Workflow Modules**
#### **`approval_workflows`** - Master Workflow Definition
- **Purpose**: Mendefinisikan template workflow approval
- **Key Fields**:
- `id`: Primary key
- `name`: Nama workflow (e.g., "Standard Approval", "Fast Track")
- `description`: Deskripsi workflow
- `is_default`: Apakah workflow default untuk client
- `is_active`: Status aktif workflow
- `client_id`: Client yang memiliki workflow
#### **`approval_workflow_steps`** - Workflow Steps Definition
- **Purpose**: Mendefinisikan step-step dalam workflow
- **Key Fields**:
- `id`: Primary key
- `workflow_id`: Foreign key ke `approval_workflows`
- `step_order`: Urutan step (1, 2, 3, dst.)
- `step_name`: Nama step (e.g., "Level 2 Review", "Level 1 Final Approval")
- `required_user_level_id`: User level yang harus approve step ini
- `is_required`: Apakah step wajib atau optional
### **2. Execution Modules**
#### **`article_approval_flows`** - Active Approval Instances
- **Purpose**: Instance aktif dari workflow yang sedang berjalan
- **Key Fields**:
- `id`: Primary key
- `article_id`: Foreign key ke `articles`
- `workflow_id`: Foreign key ke `approval_workflows`
- `current_step`: Step saat ini yang sedang menunggu approval
- `status_id`: Status (1=pending, 2=approved, 3=rejected, 4=revision_requested)
- `submitted_by_id`: User yang submit artikel
- `submitted_at`: Waktu submit
- `completed_at`: Waktu selesai (jika approved/rejected)
#### **`article_approval_step_logs`** - Step Execution History
- **Purpose**: Log setiap step yang telah dieksekusi
- **Key Fields**:
- `id`: Primary key
- `approval_flow_id`: Foreign key ke `article_approval_flows`
- `step_order`: Urutan step yang dieksekusi
- `step_name`: Nama step
- `approved_by_id`: User yang approve step ini
- `action`: Aksi yang dilakukan (approve, reject, auto_skip)
- `message`: Pesan dari approver
- `processed_at`: Waktu eksekusi
### **3. Legacy & Configuration Modules**
#### **`article_approvals`** - Legacy Approval System
- **Purpose**: Sistem approval legacy untuk backward compatibility
- **Key Fields**:
- `id`: Primary key
- `article_id`: Foreign key ke `articles`
- `approval_by`: User yang approve
- `status_id`: Status approval
- `approval_at_level`: Level yang approve
- `message`: Pesan approval
#### **`client_approval_settings`** - Client Configuration
- **Purpose**: Konfigurasi approval untuk setiap client
- **Key Fields**:
- `id`: Primary key
- `client_id`: Foreign key ke `clients`
- `workflow_id`: Workflow default untuk client
- `auto_approve_levels`: Level yang auto-approve
- `notification_settings`: Konfigurasi notifikasi
## 🔄 **Approval Workflow Process Flow**
### **Phase 1: Setup & Configuration**
```mermaid
graph TD
A[Client Setup] --> B[Create Approval Workflow]
B --> C[Define Workflow Steps]
C --> D[Set Required User Levels]
D --> E[Configure Client Settings]
E --> F[Activate Workflow]
```
**Steps:**
1. **Client Registration**: Client baru dibuat di sistem
2. **Workflow Creation**: Admin membuat workflow approval
3. **Step Definition**: Mendefinisikan step-step dengan user level requirement
4. **Client Configuration**: Set workflow default untuk client
### **Phase 2: Article Submission**
```mermaid
graph TD
A[User Creates Article] --> B{User Level Requires Approval?}
B -->|Yes| C[Create Article Approval Flow]
B -->|No| D[Auto Publish]
C --> E[Set Current Step = 1]
E --> F[Status = Pending]
F --> G[Create Legacy Approval Record]
G --> H[Notify Approvers]
```
**Database Changes:**
- `articles`: `workflow_id`, `current_approval_step = 1`, `status_id = 1`
- `article_approval_flows`: New record dengan `current_step = 1`
- `article_approvals`: Legacy record untuk backward compatibility
### **Phase 3: Step-by-Step Approval**
```mermaid
graph TD
A[Approver Reviews Article] --> B{Approve or Reject?}
B -->|Approve| C[Create Step Log]
B -->|Reject| D[Update Status to Rejected]
C --> E{More Steps?}
E -->|Yes| F[Move to Next Step]
E -->|No| G[Complete Approval]
F --> H[Update Current Step]
H --> I[Notify Next Approver]
G --> J[Update Article Status to Approved]
D --> K[Notify Submitter]
```
**Database Changes per Step:**
- `article_approval_step_logs`: New log record
- `article_approval_flows`: Update `current_step` dan `status_id`
- `articles`: Update `current_approval_step`
### **Phase 4: Completion**
```mermaid
graph TD
A[All Steps Approved] --> B[Update Final Status]
B --> C[Set Completed At]
C --> D[Publish Article]
D --> E[Send Notifications]
E --> F[Update Analytics]
```
## 📊 **Module Relationships Diagram**
```mermaid
erDiagram
CLIENTS ||--o{ APPROVAL_WORKFLOWS : "has"
CLIENTS ||--o{ CLIENT_APPROVAL_SETTINGS : "configures"
APPROVAL_WORKFLOWS ||--o{ APPROVAL_WORKFLOW_STEPS : "contains"
APPROVAL_WORKFLOWS ||--o{ ARTICLE_APPROVAL_FLOWS : "instantiates"
ARTICLES ||--o{ ARTICLE_APPROVAL_FLOWS : "has"
ARTICLES ||--o{ ARTICLE_APPROVALS : "has_legacy"
ARTICLE_APPROVAL_FLOWS ||--o{ ARTICLE_APPROVAL_STEP_LOGS : "logs"
ARTICLE_APPROVAL_FLOWS }o--|| APPROVAL_WORKFLOWS : "uses"
USERS ||--o{ ARTICLE_APPROVAL_FLOWS : "submits"
USERS ||--o{ ARTICLE_APPROVAL_STEP_LOGS : "approves"
USER_LEVELS ||--o{ APPROVAL_WORKFLOW_STEPS : "requires"
USER_LEVELS ||--o{ USERS : "defines"
```
## 🔧 **Technical Implementation Details**
### **1. Dynamic Workflow Assignment**
```go
// Ketika artikel dibuat
if userLevel.RequiresApproval {
// 1. Cari workflow default untuk client
workflow := getDefaultWorkflow(clientId)
// 2. Buat approval flow instance
approvalFlow := ArticleApprovalFlows{
ArticleId: articleId,
WorkflowId: workflow.ID,
CurrentStep: 1,
StatusId: 1, // pending
}
// 3. Buat legacy record
legacyApproval := ArticleApprovals{
ArticleId: articleId,
ApprovalAtLevel: nextApprovalLevel,
}
}
```
### **2. Step Progression Logic**
```go
// Ketika step di-approve
func ApproveStep(flowId, approvedById, message) {
// 1. Log step yang di-approve
stepLog := ArticleApprovalStepLogs{
ApprovalFlowId: flowId,
StepOrder: currentStep,
ApprovedById: approvedById,
Action: "approve",
}
// 2. Cek apakah ada next step
nextStep := getNextStep(workflowId, currentStep)
if nextStep != nil {
// 3. Pindah ke step berikutnya
updateCurrentStep(flowId, nextStep.StepOrder)
} else {
// 4. Complete approval
completeApproval(flowId)
}
}
```
### **3. User Level Hierarchy**
```go
// Level hierarchy (level_number field)
Level 1: Highest Authority (POLDAS)
Level 2: Mid Authority (POLDAS)
Level 3: Lower Authority (POLRES)
// Approval flow
Level 3 → Level 2 → Level 1 → Approved
```
## 📈 **Data Flow Examples**
### **Example 1: Standard 3-Step Approval**
```
1. User Level 3 creates article
├── article_approval_flows: {current_step: 1, status: pending}
├── article_approvals: {approval_at_level: 2}
└── articles: {current_approval_step: 1, status: pending}
2. User Level 2 approves
├── article_approval_step_logs: {step_order: 1, action: approve}
├── article_approval_flows: {current_step: 2, status: pending}
└── articles: {current_approval_step: 2}
3. User Level 1 approves
├── article_approval_step_logs: {step_order: 2, action: approve}
├── article_approval_flows: {status: approved, completed_at: now}
└── articles: {status: approved, published: true}
```
### **Example 2: Auto-Skip Logic**
```
1. User Level 1 creates article (highest authority)
├── Check: Can skip all steps?
├── article_approval_flows: {status: approved, completed_at: now}
└── articles: {status: approved, published: true}
```
## 🚀 **API Endpoints Flow**
### **Article Creation**
```
POST /api/articles
├── Check user level
├── Create article
├── Assign workflow (if needed)
├── Create approval flow
└── Return article data
```
### **Approval Process**
```
PUT /api/article-approval-flows/{id}/approve
├── Validate approver permissions
├── Create step log
├── Check for next step
├── Update flow status
└── Send notifications
```
### **Status Checking**
```
GET /api/articles/{id}/approval-status
├── Get current flow
├── Get step logs
├── Get next step info
└── Return status data
```
## 🔍 **Debugging & Monitoring**
### **Key Queries for Monitoring**
```sql
-- Active approval flows
SELECT aaf.*, a.title, ul.name as current_approver_level
FROM article_approval_flows aaf
JOIN articles a ON aaf.article_id = a.id
JOIN approval_workflow_steps aws ON aaf.workflow_id = aws.workflow_id
AND aaf.current_step = aws.step_order
JOIN user_levels ul ON aws.required_user_level_id = ul.id
WHERE aaf.status_id = 1;
-- Approval history
SELECT aasl.*, u.name as approver_name, ul.name as level_name
FROM article_approval_step_logs aasl
JOIN users u ON aasl.approved_by_id = u.id
JOIN user_levels ul ON aasl.user_level_id = ul.id
WHERE aasl.approval_flow_id = ?;
```
## ⚠️ **Common Issues & Solutions**
### **Issue 1: Step Not Progressing**
- **Cause**: `getUserLevelId` returning wrong level
- **Solution**: Fix user level mapping logic
### **Issue 2: Wrong Approval Level**
- **Cause**: `findNextApprovalLevel` logic incorrect
- **Solution**: Fix level hierarchy comparison
### **Issue 3: Missing Step Logs**
- **Cause**: Step log not created during approval
- **Solution**: Ensure step log creation in `ApproveStep`
## 📝 **Best Practices**
1. **Always create step logs** for audit trail
2. **Validate user permissions** before approval
3. **Use transactions** for multi-table updates
4. **Implement proper error handling** for edge cases
5. **Log all approval actions** for debugging
6. **Test with different user level combinations**
## 🎯 **Summary**
Sistem approval workflow MEDOLS menggunakan arsitektur modular yang memisahkan:
- **Configuration** (workflows, steps, settings)
- **Execution** (flows, logs)
- **Legacy Support** (article_approvals)
Setiap module memiliki tanggung jawab spesifik, dan mereka bekerja sama untuk menciptakan sistem approval yang fleksibel dan dapat di-audit.

View File

@ -0,0 +1,255 @@
# Approval Workflow Flow Diagram
## 🔄 **Complete Approval Process Flow**
### **1. System Setup Phase**
```
┌─────────────────┐ ┌──────────────────────┐ ┌─────────────────────┐
│ CLIENT │───▶│ APPROVAL_WORKFLOWS │───▶│ APPROVAL_WORKFLOW │
│ CREATION │ │ (Master Template) │ │ STEPS │
└─────────────────┘ └──────────────────────┘ └─────────────────────┘
┌──────────────────────┐
│ CLIENT_APPROVAL_ │
│ SETTINGS │
│ (Configuration) │
└──────────────────────┘
```
### **2. Article Submission Phase**
```
┌─────────────────┐ ┌──────────────────────┐ ┌─────────────────────┐
│ USER LEVEL 3 │───▶│ CREATE ARTICLE │───▶│ CHECK USER LEVEL │
│ (POLRES) │ │ │ │ REQUIRES APPROVAL? │
└─────────────────┘ └──────────────────────┘ └─────────────────────┘
┌──────────────────────┐
│ YES - CREATE │
│ APPROVAL FLOW │
└──────────────────────┘
┌──────────────────────┐
│ ARTICLE_APPROVAL_ │
│ FLOWS │
│ (current_step: 1) │
└──────────────────────┘
```
### **3. Step-by-Step Approval Process**
```
STEP 1: Level 2 Approval
┌─────────────────┐ ┌──────────────────────┐ ┌─────────────────────┐
│ USER LEVEL 2 │───▶│ REVIEW ARTICLE │───▶│ APPROVE/REJECT │
│ (POLDAS) │ │ (Step 1) │ │ │
└─────────────────┘ └──────────────────────┘ └─────────────────────┘
┌──────────────────────┐
│ CREATE STEP LOG │
│ (ARTICLE_APPROVAL_ │
│ STEP_LOGS) │
└──────────────────────┘
┌──────────────────────┐
│ MOVE TO STEP 2 │
│ (current_step: 2) │
└──────────────────────┘
STEP 2: Level 1 Approval
┌─────────────────┐ ┌──────────────────────┐ ┌─────────────────────┐
│ USER LEVEL 1 │───▶│ REVIEW ARTICLE │───▶│ APPROVE/REJECT │
│ (POLDAS) │ │ (Step 2) │ │ │
└─────────────────┘ └──────────────────────┘ └─────────────────────┘
┌──────────────────────┐
│ CREATE STEP LOG │
│ (ARTICLE_APPROVAL_ │
│ STEP_LOGS) │
└──────────────────────┘
┌──────────────────────┐
│ COMPLETE APPROVAL │
│ (status: approved) │
└──────────────────────┘
```
## 📊 **Database State Changes**
### **Initial State (Article Created)**
```
ARTICLES:
├── id: 101
├── title: "Sample Article"
├── workflow_id: 1
├── current_approval_step: 1
├── status_id: 1 (pending)
└── created_by_id: 3
ARTICLE_APPROVAL_FLOWS:
├── id: 1
├── article_id: 101
├── workflow_id: 1
├── current_step: 1
├── status_id: 1 (pending)
├── submitted_by_id: 3
└── submitted_at: 2025-01-15 10:00:00
ARTICLE_APPROVALS (Legacy):
├── id: 1
├── article_id: 101
├── approval_at_level: 2
├── status_id: 1 (pending)
└── message: "Need Approval"
```
### **After Step 1 Approval (Level 2)**
```
ARTICLES:
├── current_approval_step: 2 ← UPDATED
└── status_id: 1 (pending)
ARTICLE_APPROVAL_FLOWS:
├── current_step: 2 ← UPDATED
└── status_id: 1 (pending)
ARTICLE_APPROVAL_STEP_LOGS: ← NEW RECORD
├── id: 1
├── approval_flow_id: 1
├── step_order: 1
├── step_name: "Level 2 Review"
├── approved_by_id: 2
├── action: "approve"
├── message: "Approved by Level 2"
└── processed_at: 2025-01-15 11:00:00
```
### **After Step 2 Approval (Level 1) - Final**
```
ARTICLES:
├── current_approval_step: null ← UPDATED
├── status_id: 2 (approved) ← UPDATED
├── is_publish: true ← UPDATED
└── published_at: 2025-01-15 12:00:00 ← UPDATED
ARTICLE_APPROVAL_FLOWS:
├── current_step: 2
├── status_id: 2 (approved) ← UPDATED
└── completed_at: 2025-01-15 12:00:00 ← UPDATED
ARTICLE_APPROVAL_STEP_LOGS: ← NEW RECORD
├── id: 2
├── approval_flow_id: 1
├── step_order: 2
├── step_name: "Level 1 Final Approval"
├── approved_by_id: 1
├── action: "approve"
├── message: "Final approval by Level 1"
└── processed_at: 2025-01-15 12:00:00
```
## 🔧 **Module Interaction Matrix**
| Module | Purpose | Reads From | Writes To | Triggers |
|--------|---------|------------|-----------|----------|
| `articles` | Article Management | - | `articles` | Creates approval flow |
| `approval_workflows` | Workflow Templates | `approval_workflows` | - | Defines steps |
| `approval_workflow_steps` | Step Definitions | `approval_workflow_steps` | - | Defines requirements |
| `article_approval_flows` | Active Instances | `approval_workflows` | `article_approval_flows` | Manages progression |
| `article_approval_step_logs` | Audit Trail | `article_approval_flows` | `article_approval_step_logs` | Logs actions |
| `article_approvals` | Legacy Support | - | `article_approvals` | Backward compatibility |
| `client_approval_settings` | Configuration | `clients` | `client_approval_settings` | Sets defaults |
## 🚨 **Error Handling Flow**
```
┌─────────────────┐ ┌──────────────────────┐ ┌─────────────────────┐
│ ERROR │───▶│ LOG ERROR │───▶│ ROLLBACK │
│ OCCURS │ │ (Zerolog) │ │ TRANSACTION │
└─────────────────┘ └──────────────────────┘ └─────────────────────┘
┌──────────────────────┐
│ NOTIFY USER │
│ (Error Response) │
└──────────────────────┘
```
## 🔄 **Auto-Skip Logic Flow**
```
┌─────────────────┐ ┌──────────────────────┐ ┌─────────────────────┐
│ USER LEVEL 1 │───▶│ CHECK CAN SKIP │───▶│ SKIP ALL STEPS │
│ (HIGHEST) │ │ ALL STEPS? │ │ │
└─────────────────┘ └──────────────────────┘ └─────────────────────┘
┌──────────────────────┐
│ AUTO APPROVE │
│ (status: approved) │
└──────────────────────┘
```
## 📈 **Performance Considerations**
### **Database Indexes Needed**
```sql
-- For fast approval flow lookups
CREATE INDEX idx_article_approval_flows_status ON article_approval_flows(status_id);
CREATE INDEX idx_article_approval_flows_current_step ON article_approval_flows(current_step);
-- For step log queries
CREATE INDEX idx_article_approval_step_logs_flow_id ON article_approval_step_logs(approval_flow_id);
CREATE INDEX idx_article_approval_step_logs_processed_at ON article_approval_step_logs(processed_at);
-- For user level queries
CREATE INDEX idx_users_user_level_id ON users(user_level_id);
CREATE INDEX idx_user_levels_level_number ON user_levels(level_number);
```
### **Caching Strategy**
```
1. Workflow Templates → Cache in Redis
2. User Level Mappings → Cache in Memory
3. Active Approval Flows → Cache with TTL
4. Step Requirements → Cache per Workflow
```
## 🎯 **Key Success Metrics**
1. **Approval Time**: Average time from submission to approval
2. **Step Completion Rate**: Percentage of steps completed successfully
3. **Error Rate**: Percentage of failed approval processes
4. **User Satisfaction**: Feedback on approval process
5. **System Performance**: Response times for approval actions
## 🔍 **Debugging Checklist**
- [ ] Check if workflow exists and is active
- [ ] Verify user level permissions
- [ ] Confirm step order is correct
- [ ] Validate foreign key relationships
- [ ] Check transaction rollback scenarios
- [ ] Monitor step log creation
- [ ] Verify notification delivery
- [ ] Test edge cases (auto-skip, rejection, etc.)
## 📝 **Summary**
Sistem approval workflow MEDOLS menggunakan pendekatan modular yang memungkinkan:
1. **Fleksibilitas**: Workflow dapat dikonfigurasi per client
2. **Auditability**: Setiap action dicatat dalam step logs
3. **Scalability**: Dapat menangani multiple workflow types
4. **Backward Compatibility**: Legacy system tetap berfungsi
5. **User Experience**: Auto-skip untuk user level tinggi
Dengan arsitektur ini, sistem dapat menangani berbagai skenario approval dengan efisien dan dapat diandalkan.

View File

@ -0,0 +1,440 @@
# Database Relationships & Entity Details
## 🗄️ **Entity Relationship Overview**
### **Core Entities**
#### **1. CLIENTS**
```sql
CREATE TABLE clients (
id UUID PRIMARY KEY,
name VARCHAR NOT NULL,
is_active BOOLEAN DEFAULT true,
created_at TIMESTAMP DEFAULT NOW(),
updated_at TIMESTAMP DEFAULT NOW()
);
```
#### **2. USER_LEVELS**
```sql
CREATE TABLE user_levels (
id SERIAL PRIMARY KEY,
name VARCHAR NOT NULL,
alias_name VARCHAR NOT NULL,
level_number INTEGER NOT NULL, -- 1=Highest, 2=Mid, 3=Lowest
parent_level_id INTEGER REFERENCES user_levels(id),
province_id INTEGER,
group VARCHAR,
is_approval_active BOOLEAN DEFAULT false,
client_id UUID REFERENCES clients(id),
is_active BOOLEAN DEFAULT true,
created_at TIMESTAMP DEFAULT NOW(),
updated_at TIMESTAMP DEFAULT NOW()
);
```
#### **3. USERS**
```sql
CREATE TABLE users (
id SERIAL PRIMARY KEY,
name VARCHAR NOT NULL,
email VARCHAR UNIQUE NOT NULL,
user_level_id INTEGER REFERENCES user_levels(id),
client_id UUID REFERENCES clients(id),
is_active BOOLEAN DEFAULT true,
created_at TIMESTAMP DEFAULT NOW(),
updated_at TIMESTAMP DEFAULT NOW()
);
```
### **Workflow Configuration Entities**
#### **4. APPROVAL_WORKFLOWS**
```sql
CREATE TABLE approval_workflows (
id SERIAL PRIMARY KEY,
name VARCHAR NOT NULL,
description TEXT,
is_default BOOLEAN DEFAULT false,
is_active BOOLEAN DEFAULT true,
client_id UUID REFERENCES clients(id),
created_at TIMESTAMP DEFAULT NOW(),
updated_at TIMESTAMP DEFAULT NOW()
);
```
#### **5. APPROVAL_WORKFLOW_STEPS**
```sql
CREATE TABLE approval_workflow_steps (
id SERIAL PRIMARY KEY,
workflow_id INTEGER REFERENCES approval_workflows(id) ON DELETE CASCADE,
step_order INTEGER NOT NULL, -- 1, 2, 3, etc.
step_name VARCHAR NOT NULL,
required_user_level_id INTEGER REFERENCES user_levels(id),
is_required BOOLEAN DEFAULT true,
created_at TIMESTAMP DEFAULT NOW(),
updated_at TIMESTAMP DEFAULT NOW()
);
```
#### **6. CLIENT_APPROVAL_SETTINGS**
```sql
CREATE TABLE client_approval_settings (
id SERIAL PRIMARY KEY,
client_id UUID REFERENCES clients(id),
workflow_id INTEGER REFERENCES approval_workflows(id),
auto_approve_levels JSONB, -- Array of level numbers
notification_settings JSONB,
created_at TIMESTAMP DEFAULT NOW(),
updated_at TIMESTAMP DEFAULT NOW()
);
```
### **Article & Approval Execution Entities**
#### **7. ARTICLES**
```sql
CREATE TABLE articles (
id SERIAL PRIMARY KEY,
title VARCHAR NOT NULL,
slug VARCHAR UNIQUE,
description TEXT,
html_description TEXT,
workflow_id INTEGER REFERENCES approval_workflows(id),
current_approval_step INTEGER,
status_id INTEGER, -- 1=pending, 2=approved, 3=rejected
is_publish BOOLEAN DEFAULT false,
published_at TIMESTAMP,
created_by_id INTEGER REFERENCES users(id),
client_id UUID REFERENCES clients(id),
created_at TIMESTAMP DEFAULT NOW(),
updated_at TIMESTAMP DEFAULT NOW()
);
```
#### **8. ARTICLE_APPROVAL_FLOWS**
```sql
CREATE TABLE article_approval_flows (
id SERIAL PRIMARY KEY,
article_id INTEGER REFERENCES articles(id) ON DELETE CASCADE,
workflow_id INTEGER REFERENCES approval_workflows(id),
current_step INTEGER DEFAULT 1,
status_id INTEGER DEFAULT 1, -- 1=pending, 2=approved, 3=rejected, 4=revision_requested
submitted_by_id INTEGER REFERENCES users(id),
submitted_at TIMESTAMP DEFAULT NOW(),
completed_at TIMESTAMP,
rejection_reason TEXT,
revision_requested BOOLEAN DEFAULT false,
revision_message TEXT,
client_id UUID REFERENCES clients(id),
created_at TIMESTAMP DEFAULT NOW(),
updated_at TIMESTAMP DEFAULT NOW()
);
```
#### **9. ARTICLE_APPROVAL_STEP_LOGS**
```sql
CREATE TABLE article_approval_step_logs (
id SERIAL PRIMARY KEY,
approval_flow_id INTEGER REFERENCES article_approval_flows(id) ON DELETE CASCADE,
step_order INTEGER NOT NULL,
step_name VARCHAR NOT NULL,
approved_by_id INTEGER REFERENCES users(id),
action VARCHAR NOT NULL, -- approve, reject, auto_skip, request_revision
message TEXT,
processed_at TIMESTAMP DEFAULT NOW(),
user_level_id INTEGER REFERENCES user_levels(id),
created_at TIMESTAMP DEFAULT NOW()
);
```
#### **10. ARTICLE_APPROVALS (Legacy)**
```sql
CREATE TABLE article_approvals (
id SERIAL PRIMARY KEY,
article_id INTEGER REFERENCES articles(id) ON DELETE CASCADE,
approval_by INTEGER REFERENCES users(id),
status_id INTEGER, -- 1=pending, 2=approved, 3=rejected
message TEXT,
approval_at_level INTEGER REFERENCES user_levels(id),
approval_at TIMESTAMP DEFAULT NOW(),
created_at TIMESTAMP DEFAULT NOW()
);
```
## 🔗 **Relationship Details**
### **One-to-Many Relationships**
```
CLIENTS (1) ──→ (N) USER_LEVELS
CLIENTS (1) ──→ (N) USERS
CLIENTS (1) ──→ (N) APPROVAL_WORKFLOWS
CLIENTS (1) ──→ (N) ARTICLES
CLIENTS (1) ──→ (N) CLIENT_APPROVAL_SETTINGS
USER_LEVELS (1) ──→ (N) USERS
USER_LEVELS (1) ──→ (N) APPROVAL_WORKFLOW_STEPS
USER_LEVELS (1) ──→ (N) ARTICLE_APPROVAL_STEP_LOGS
APPROVAL_WORKFLOWS (1) ──→ (N) APPROVAL_WORKFLOW_STEPS
APPROVAL_WORKFLOWS (1) ──→ (N) ARTICLE_APPROVAL_FLOWS
APPROVAL_WORKFLOWS (1) ──→ (N) ARTICLES
ARTICLES (1) ──→ (N) ARTICLE_APPROVAL_FLOWS
ARTICLES (1) ──→ (N) ARTICLE_APPROVALS
ARTICLE_APPROVAL_FLOWS (1) ──→ (N) ARTICLE_APPROVAL_STEP_LOGS
USERS (1) ──→ (N) ARTICLES (as creator)
USERS (1) ──→ (N) ARTICLE_APPROVAL_FLOWS (as submitter)
USERS (1) ──→ (N) ARTICLE_APPROVAL_STEP_LOGS (as approver)
USERS (1) ──→ (N) ARTICLE_APPROVALS (as approver)
```
### **Many-to-Many Relationships (Implicit)**
```
USERS ←→ USER_LEVELS (through user_level_id)
ARTICLES ←→ APPROVAL_WORKFLOWS (through workflow_id)
ARTICLE_APPROVAL_FLOWS ←→ APPROVAL_WORKFLOW_STEPS (through workflow_id and step_order)
```
## 📊 **Data Flow Examples**
### **Example 1: Complete Approval Process**
#### **Step 1: Setup Data**
```sql
-- Client
INSERT INTO clients (id, name) VALUES ('b1ce6602-07ad-46c2-85eb-0cd6decfefa3', 'Test Client');
-- User Levels
INSERT INTO user_levels (name, alias_name, level_number, is_approval_active, client_id) VALUES
('POLDAS', 'Poldas', 1, true, 'b1ce6602-07ad-46c2-85eb-0cd6decfefa3'),
('POLDAS', 'Poldas', 2, true, 'b1ce6602-07ad-46c2-85eb-0cd6decfefa3'),
('POLRES', 'Polres', 3, true, 'b1ce6602-07ad-46c2-85eb-0cd6decfefa3');
-- Users
INSERT INTO users (name, email, user_level_id, client_id) VALUES
('Admin Level 1', 'admin1@test.com', 1, 'b1ce6602-07ad-46c2-85eb-0cd6decfefa3'),
('Admin Level 2', 'admin2@test.com', 2, 'b1ce6602-07ad-46c2-85eb-0cd6decfefa3'),
('User Level 3', 'user3@test.com', 3, 'b1ce6602-07ad-46c2-85eb-0cd6decfefa3');
-- Workflow
INSERT INTO approval_workflows (name, description, is_default, client_id) VALUES
('Standard Approval', 'Standard 3-step approval process', true, 'b1ce6602-07ad-46c2-85eb-0cd6decfefa3');
-- Workflow Steps
INSERT INTO approval_workflow_steps (workflow_id, step_order, step_name, required_user_level_id) VALUES
(1, 1, 'Level 2 Review', 2),
(1, 2, 'Level 1 Final Approval', 1);
```
#### **Step 2: Article Creation**
```sql
-- Article created by User Level 3
INSERT INTO articles (title, slug, workflow_id, current_approval_step, status_id, created_by_id, client_id) VALUES
('Test Article', 'test-article', 1, 1, 1, 3, 'b1ce6602-07ad-46c2-85eb-0cd6decfefa3');
-- Approval Flow
INSERT INTO article_approval_flows (article_id, workflow_id, current_step, status_id, submitted_by_id, client_id) VALUES
(1, 1, 1, 1, 3, 'b1ce6602-07ad-46c2-85eb-0cd6decfefa3');
-- Legacy Approval
INSERT INTO article_approvals (article_id, approval_by, status_id, message, approval_at_level) VALUES
(1, 3, 1, 'Need Approval', 2);
```
#### **Step 3: Level 2 Approval**
```sql
-- Step Log
INSERT INTO article_approval_step_logs (approval_flow_id, step_order, step_name, approved_by_id, action, message, user_level_id) VALUES
(1, 1, 'Level 2 Review', 2, 'approve', 'Approved by Level 2', 2);
-- Update Flow
UPDATE article_approval_flows SET current_step = 2 WHERE id = 1;
-- Update Article
UPDATE articles SET current_approval_step = 2 WHERE id = 1;
```
#### **Step 4: Level 1 Final Approval**
```sql
-- Step Log
INSERT INTO article_approval_step_logs (approval_flow_id, step_order, step_name, approved_by_id, action, message, user_level_id) VALUES
(1, 2, 'Level 1 Final Approval', 1, 'approve', 'Final approval by Level 1', 1);
-- Complete Flow
UPDATE article_approval_flows SET
status_id = 2,
completed_at = NOW()
WHERE id = 1;
-- Publish Article
UPDATE articles SET
status_id = 2,
is_publish = true,
published_at = NOW(),
current_approval_step = NULL
WHERE id = 1;
```
## 🔍 **Key Queries for Monitoring**
### **Active Approval Flows**
```sql
SELECT
aaf.id as flow_id,
a.title as article_title,
aaf.current_step,
aws.step_name as current_step_name,
ul.name as required_approver_level,
u.name as submitted_by,
aaf.submitted_at,
aaf.status_id
FROM article_approval_flows aaf
JOIN articles a ON aaf.article_id = a.id
JOIN approval_workflow_steps aws ON aaf.workflow_id = aws.workflow_id
AND aaf.current_step = aws.step_order
JOIN user_levels ul ON aws.required_user_level_id = ul.id
JOIN users u ON aaf.submitted_by_id = u.id
WHERE aaf.status_id = 1 -- pending
ORDER BY aaf.submitted_at DESC;
```
### **Approval History**
```sql
SELECT
a.title as article_title,
aasl.step_name,
aasl.action,
aasl.message,
u.name as approver_name,
ul.name as approver_level,
aasl.processed_at
FROM article_approval_step_logs aasl
JOIN article_approval_flows aaf ON aasl.approval_flow_id = aaf.id
JOIN articles a ON aaf.article_id = a.id
JOIN users u ON aasl.approved_by_id = u.id
JOIN user_levels ul ON aasl.user_level_id = ul.id
WHERE aaf.article_id = ? -- specific article
ORDER BY aasl.step_order, aasl.processed_at;
```
### **Workflow Performance**
```sql
SELECT
aw.name as workflow_name,
COUNT(aaf.id) as total_flows,
COUNT(CASE WHEN aaf.status_id = 2 THEN 1 END) as approved,
COUNT(CASE WHEN aaf.status_id = 3 THEN 1 END) as rejected,
AVG(EXTRACT(EPOCH FROM (aaf.completed_at - aaf.submitted_at))/3600) as avg_hours
FROM approval_workflows aw
LEFT JOIN article_approval_flows aaf ON aw.id = aaf.workflow_id
WHERE aw.client_id = ?
GROUP BY aw.id, aw.name;
```
## ⚠️ **Common Data Issues**
### **Issue 1: Orphaned Records**
```sql
-- Find articles without approval flows
SELECT a.id, a.title
FROM articles a
LEFT JOIN article_approval_flows aaf ON a.id = aaf.article_id
WHERE aaf.id IS NULL AND a.workflow_id IS NOT NULL;
-- Find approval flows without articles
SELECT aaf.id, aaf.article_id
FROM article_approval_flows aaf
LEFT JOIN articles a ON aaf.article_id = a.id
WHERE a.id IS NULL;
```
### **Issue 2: Inconsistent Step Data**
```sql
-- Find flows with invalid current_step
SELECT aaf.id, aaf.current_step, aws.step_order
FROM article_approval_flows aaf
JOIN approval_workflow_steps aws ON aaf.workflow_id = aws.workflow_id
WHERE aaf.current_step != aws.step_order;
```
### **Issue 3: Missing Step Logs**
```sql
-- Find approved flows without step logs
SELECT aaf.id, aaf.article_id, aaf.status_id
FROM article_approval_flows aaf
LEFT JOIN article_approval_step_logs aasl ON aaf.id = aasl.approval_flow_id
WHERE aaf.status_id = 2 AND aasl.id IS NULL;
```
## 🚀 **Performance Optimization**
### **Indexes**
```sql
-- Primary indexes
CREATE INDEX idx_article_approval_flows_status ON article_approval_flows(status_id);
CREATE INDEX idx_article_approval_flows_current_step ON article_approval_flows(current_step);
CREATE INDEX idx_article_approval_flows_client_id ON article_approval_flows(client_id);
CREATE INDEX idx_article_approval_step_logs_flow_id ON article_approval_step_logs(approval_flow_id);
CREATE INDEX idx_article_approval_step_logs_processed_at ON article_approval_step_logs(processed_at);
CREATE INDEX idx_articles_workflow_id ON articles(workflow_id);
CREATE INDEX idx_articles_status_id ON articles(status_id);
CREATE INDEX idx_users_user_level_id ON users(user_level_id);
CREATE INDEX idx_user_levels_level_number ON user_levels(level_number);
-- Composite indexes
CREATE INDEX idx_article_approval_flows_status_step ON article_approval_flows(status_id, current_step);
CREATE INDEX idx_approval_workflow_steps_workflow_order ON approval_workflow_steps(workflow_id, step_order);
```
### **Partitioning (for large datasets)**
```sql
-- Partition article_approval_step_logs by month
CREATE TABLE article_approval_step_logs_2025_01 PARTITION OF article_approval_step_logs
FOR VALUES FROM ('2025-01-01') TO ('2025-02-01');
```
## 📝 **Data Validation Rules**
### **Business Rules**
1. **Workflow Steps**: Must be sequential (1, 2, 3, etc.)
2. **User Levels**: Level number must be unique per client
3. **Approval Flows**: Can only have one active flow per article
4. **Step Logs**: Must have corresponding workflow step
5. **Status Transitions**: Only valid transitions allowed
### **Database Constraints**
```sql
-- Ensure step order is sequential
ALTER TABLE approval_workflow_steps
ADD CONSTRAINT chk_step_order_positive CHECK (step_order > 0);
-- Ensure level number is positive
ALTER TABLE user_levels
ADD CONSTRAINT chk_level_number_positive CHECK (level_number > 0);
-- Ensure status values are valid
ALTER TABLE article_approval_flows
ADD CONSTRAINT chk_status_valid CHECK (status_id IN (1, 2, 3, 4));
-- Ensure current step is valid
ALTER TABLE article_approval_flows
ADD CONSTRAINT chk_current_step_positive CHECK (current_step > 0);
```
## 🎯 **Summary**
Database design untuk sistem approval workflow MEDOLS menggunakan:
1. **Normalized Structure**: Memisahkan konfigurasi dari eksekusi
2. **Audit Trail**: Step logs untuk tracking lengkap
3. **Flexibility**: Workflow dapat dikonfigurasi per client
4. **Performance**: Indexes untuk query yang efisien
5. **Data Integrity**: Constraints dan validasi
6. **Scalability**: Partitioning untuk data besar
Dengan struktur ini, sistem dapat menangani berbagai skenario approval dengan efisien dan dapat diandalkan.

View File

@ -0,0 +1,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

View File

@ -0,0 +1,785 @@
# End-to-End Testing Scenarios - Approval Workflow System
## Overview
Dokumentasi ini berisi skenario testing end-to-end lengkap untuk sistem approval workflow, mulai dari pembuatan client baru hingga pembuatan artikel dengan proses approval yang dinamis.
## Base Configuration
```bash
# Base URL
BASE_URL="http://localhost:8800/api"
# Headers
AUTH_HEADER="Authorization: Bearer YOUR_JWT_TOKEN"
CLIENT_HEADER="X-Client-Key: YOUR_CLIENT_KEY"
CONTENT_TYPE="Content-Type: application/json"
```
---
## 🏢 Scenario 1: Complete Client Setup to Article Creation
### Step 1: Create New Client
```bash
curl -X POST "${BASE_URL}/clients" \
-H "${AUTH_HEADER}" \
-H "${CLIENT_HEADER}" \
-H "${CONTENT_TYPE}" \
-d '{
"name": "Test Media Company",
"is_active": true
}'
```
**Expected Response:**
```json
{
"success": true,
"messages": ["Client created successfully"],
"data": {
"id": "550e8400-e29b-41d4-a716-446655440000",
"name": "Test Media Company",
"is_active": true,
"created_at": "2024-01-15T10:00:00Z"
}
}
```
### Step 2: Create User Levels
```bash
# Create user levels for approval workflow
curl -X POST "${BASE_URL}/user-levels" \
-H "${AUTH_HEADER}" \
-H "${CLIENT_HEADER}" \
-H "${CONTENT_TYPE}" \
-d '{
"name": "Editor",
"alias_name": "ED",
"level_number": 1,
"is_approval_active": true
}'
curl -X POST "${BASE_URL}/user-levels" \
-H "${AUTH_HEADER}" \
-H "${CLIENT_HEADER}" \
-H "${CONTENT_TYPE}" \
-d '{
"name": "Senior Editor",
"alias_name": "SED",
"level_number": 2,
"is_approval_active": true
}'
curl -X POST "${BASE_URL}/user-levels" \
-H "${AUTH_HEADER}" \
-H "${CLIENT_HEADER}" \
-H "${CONTENT_TYPE}" \
-d '{
"name": "Editor in Chief",
"alias_name": "EIC",
"level_number": 3,
"is_approval_active": true
}'
```
### Step 3: Create Approval Workflow
```bash
curl -X POST "${BASE_URL}/approval-workflows" \
-H "${AUTH_HEADER}" \
-H "${CLIENT_HEADER}" \
-H "${CONTENT_TYPE}" \
-d '{
"name": "Standard 3-Level Editorial Review",
"description": "Complete editorial workflow with 3 approval levels",
"is_default": true,
"is_active": true,
"requires_approval": true,
"auto_publish": false,
"client_id": "550e8400-e29b-41d4-a716-446655440000"
}'
```
### Step 4: Create Workflow Steps
```bash
# Step 1: Editor Review
curl -X POST "${BASE_URL}/approval-workflow-steps" \
-H "${AUTH_HEADER}" \
-H "${CLIENT_HEADER}" \
-H "${CONTENT_TYPE}" \
-d '{
"workflow_id": 1,
"step_order": 1,
"step_name": "Editor Review",
"required_user_level_id": 1,
"can_skip": false,
"auto_approve_after_hours": 24,
"client_id": "550e8400-e29b-41d4-a716-446655440000"
}'
# Step 2: Senior Editor Review
curl -X POST "${BASE_URL}/approval-workflow-steps" \
-H "${AUTH_HEADER}" \
-H "${CLIENT_HEADER}" \
-H "${CONTENT_TYPE}" \
-d '{
"workflow_id": 1,
"step_order": 2,
"step_name": "Senior Editor Review",
"required_user_level_id": 2,
"can_skip": false,
"auto_approve_after_hours": 48,
"client_id": "550e8400-e29b-41d4-a716-446655440000"
}'
# Step 3: Editor in Chief
curl -X POST "${BASE_URL}/approval-workflow-steps" \
-H "${AUTH_HEADER}" \
-H "${CLIENT_HEADER}" \
-H "${CONTENT_TYPE}" \
-d '{
"workflow_id": 1,
"step_order": 3,
"step_name": "Editor in Chief Approval",
"required_user_level_id": 3,
"can_skip": false,
"auto_approve_after_hours": 72,
"client_id": "550e8400-e29b-41d4-a716-446655440000"
}'
```
### Step 5: Configure Client Approval Settings
```bash
curl -X POST "${BASE_URL}/client-approval-settings" \
-H "${AUTH_HEADER}" \
-H "${CLIENT_HEADER}" \
-H "${CONTENT_TYPE}" \
-d '{
"client_id": "550e8400-e29b-41d4-a716-446655440000",
"requires_approval": true,
"default_workflow_id": 1,
"auto_publish_articles": false,
"approval_exempt_users": [],
"approval_exempt_roles": [],
"approval_exempt_categories": [],
"require_approval_for": ["article", "news", "review"],
"skip_approval_for": ["announcement", "update"],
"is_active": true
}'
```
### Step 6: Create Article Category
```bash
curl -X POST "${BASE_URL}/article-categories" \
-H "${AUTH_HEADER}" \
-H "${CLIENT_HEADER}" \
-H "${CONTENT_TYPE}" \
-d '{
"title": "Technology News",
"description": "Latest technology news and updates",
"slug": "technology-news",
"status_id": 1,
"is_publish": true,
"client_id": "550e8400-e29b-41d4-a716-446655440000"
}'
```
### Step 7: Create Article
```bash
curl -X POST "${BASE_URL}/articles" \
-H "${AUTH_HEADER}" \
-H "${CLIENT_HEADER}" \
-H "${CONTENT_TYPE}" \
-d '{
"title": "Revolutionary AI Technology Breakthrough",
"slug": "revolutionary-ai-technology-breakthrough",
"description": "A comprehensive look at the latest AI breakthrough that could change everything",
"html_description": "<p>A comprehensive look at the latest AI breakthrough that could change everything</p>",
"category_id": 1,
"type_id": 1,
"tags": "AI, Technology, Innovation, Breakthrough",
"created_by_id": 1,
"status_id": 1,
"is_draft": false,
"client_id": "550e8400-e29b-41d4-a716-446655440000"
}'
```
**Expected Response:**
```json
{
"success": true,
"messages": ["Article created successfully"],
"data": {
"id": 1,
"title": "Revolutionary AI Technology Breakthrough",
"status_id": 1,
"is_draft": false,
"approval_required": true,
"workflow_id": 1,
"created_at": "2024-01-15T10:30:00Z"
}
}
```
---
## 📝 Scenario 2: Complete Approval Process
### Step 1: Submit Article for Approval
```bash
curl -X POST "${BASE_URL}/articles/1/submit-approval" \
-H "${AUTH_HEADER}" \
-H "${CLIENT_HEADER}" \
-H "${CONTENT_TYPE}" \
-d '{
"workflow_id": 1,
"message": "Article ready for editorial review process"
}'
```
**Expected Response:**
```json
{
"success": true,
"messages": ["Article submitted for approval successfully"],
"data": {
"id": 1,
"article_id": 1,
"workflow_id": 1,
"current_step": 1,
"status_id": 1,
"submitted_at": "2024-01-15T10:35:00Z"
}
}
```
### Step 2: Check Approval Status
```bash
curl -X GET "${BASE_URL}/articles/1/approval-status" \
-H "${AUTH_HEADER}" \
-H "${CLIENT_HEADER}"
```
**Expected Response:**
```json
{
"success": true,
"messages": ["Approval status retrieved successfully"],
"data": {
"article_id": 1,
"current_status": "pending_approval",
"current_step": 1,
"total_steps": 3,
"workflow_name": "Standard 3-Level Editorial Review",
"current_step_name": "Editor Review",
"next_step_name": "Senior Editor Review",
"waiting_since": "2024-01-15T10:35:00Z"
}
}
```
### Step 3: Editor Approves (Step 1)
```bash
curl -X POST "${BASE_URL}/articles/1/approve" \
-H "${AUTH_HEADER}" \
-H "${CLIENT_HEADER}" \
-H "${CONTENT_TYPE}" \
-d '{
"message": "Content quality meets editorial standards, approved for next level"
}'
```
**Expected Response:**
```json
{
"success": true,
"messages": ["Article approved successfully"],
"data": {
"current_step": 2,
"status": "moved_to_next_level",
"next_approver_level": 2,
"approved_at": "2024-01-15T11:00:00Z"
}
}
```
### Step 4: Senior Editor Approves (Step 2)
```bash
curl -X POST "${BASE_URL}/articles/1/approve" \
-H "${AUTH_HEADER}" \
-H "${CLIENT_HEADER}" \
-H "${CONTENT_TYPE}" \
-d '{
"message": "Excellent content quality and structure, ready for final approval"
}'
```
**Expected Response:**
```json
{
"success": true,
"messages": ["Article approved successfully"],
"data": {
"current_step": 3,
"status": "moved_to_next_level",
"next_approver_level": 3,
"approved_at": "2024-01-15T12:00:00Z"
}
}
```
### Step 5: Editor in Chief Approves (Step 3 - Final)
```bash
curl -X POST "${BASE_URL}/articles/1/approve" \
-H "${AUTH_HEADER}" \
-H "${CLIENT_HEADER}" \
-H "${CONTENT_TYPE}" \
-d '{
"message": "Final approval granted, content ready for publication"
}'
```
**Expected Response:**
```json
{
"success": true,
"messages": ["Article approved and published successfully"],
"data": {
"status": "approved",
"article_status": "published",
"is_publish": true,
"published_at": "2024-01-15T13:00:00Z",
"completion_date": "2024-01-15T13:00:00Z"
}
}
```
---
## ❌ Scenario 3: Article Rejection and Revision
### Step 1: Submit Another Article
```bash
curl -X POST "${BASE_URL}/articles" \
-H "${AUTH_HEADER}" \
-H "${CLIENT_HEADER}" \
-H "${CONTENT_TYPE}" \
-d '{
"title": "Product Review: New Smartphone",
"slug": "product-review-new-smartphone",
"description": "Comprehensive review of the latest smartphone",
"html_description": "<p>Comprehensive review of the latest smartphone</p>",
"category_id": 1,
"type_id": 1,
"tags": "Review, Smartphone, Technology",
"created_by_id": 1,
"status_id": 1,
"is_draft": false,
"client_id": "550e8400-e29b-41d4-a716-446655440000"
}'
```
### Step 2: Submit for Approval
```bash
curl -X POST "${BASE_URL}/articles/2/submit-approval" \
-H "${AUTH_HEADER}" \
-H "${CLIENT_HEADER}" \
-H "${CONTENT_TYPE}" \
-d '{
"workflow_id": 1,
"message": "Product review ready for approval"
}'
```
### Step 3: Editor Approves (Step 1)
```bash
curl -X POST "${BASE_URL}/articles/2/approve" \
-H "${AUTH_HEADER}" \
-H "${CLIENT_HEADER}" \
-H "${CONTENT_TYPE}" \
-d '{
"message": "Initial review passed, good structure"
}'
```
### Step 4: Senior Editor Rejects (Step 2)
```bash
curl -X POST "${BASE_URL}/articles/2/reject" \
-H "${AUTH_HEADER}" \
-H "${CLIENT_HEADER}" \
-H "${CONTENT_TYPE}" \
-d '{
"message": "Insufficient technical details and benchmark comparisons needed"
}'
```
**Expected Response:**
```json
{
"success": true,
"messages": ["Article rejected successfully"],
"data": {
"status": "rejected",
"article_status": "draft",
"rejection_reason": "Insufficient technical details and benchmark comparisons needed",
"rejected_at": "2024-01-15T14:00:00Z"
}
}
```
### Step 5: Request Revision
```bash
curl -X POST "${BASE_URL}/articles/2/request-revision" \
-H "${AUTH_HEADER}" \
-H "${CLIENT_HEADER}" \
-H "${CONTENT_TYPE}" \
-d '{
"message": "Please add detailed technical specifications, benchmark comparisons, and more comprehensive testing results"
}'
```
**Expected Response:**
```json
{
"success": true,
"messages": ["Revision requested successfully"],
"data": {
"status": "revision_requested",
"revision_message": "Please add detailed technical specifications, benchmark comparisons, and more comprehensive testing results",
"requested_at": "2024-01-15T14:15:00Z"
}
}
```
### Step 6: Resubmit After Revision
```bash
curl -X POST "${BASE_URL}/articles/2/resubmit" \
-H "${AUTH_HEADER}" \
-H "${CLIENT_HEADER}" \
-H "${CONTENT_TYPE}" \
-d '{
"message": "Article revised with additional technical details and benchmark comparisons"
}'
```
**Expected Response:**
```json
{
"success": true,
"messages": ["Article resubmitted successfully"],
"data": {
"status": "pending_approval",
"current_step": 1,
"resubmitted_at": "2024-01-15T15:00:00Z"
}
}
```
---
## ⚡ Scenario 4: Dynamic Approval Toggle
### Step 1: Check Current Settings
```bash
curl -X GET "${BASE_URL}/client-approval-settings" \
-H "${AUTH_HEADER}" \
-H "${CLIENT_HEADER}"
```
### Step 2: Disable Approval System
```bash
curl -X PUT "${BASE_URL}/client-approval-settings/1" \
-H "${AUTH_HEADER}" \
-H "${CLIENT_HEADER}" \
-H "${CONTENT_TYPE}" \
-d '{
"requires_approval": false,
"auto_publish_articles": true,
"reason": "Breaking news mode - immediate publishing required"
}'
```
**Expected Response:**
```json
{
"success": true,
"messages": ["Approval settings updated successfully"],
"data": {
"requires_approval": false,
"auto_publish_articles": true,
"updated_at": "2024-01-15T16:00:00Z"
}
}
```
### Step 3: Create Article (Should Auto-Publish)
```bash
curl -X POST "${BASE_URL}/articles" \
-H "${AUTH_HEADER}" \
-H "${CLIENT_HEADER}" \
-H "${CONTENT_TYPE}" \
-d '{
"title": "BREAKING: Major Tech Acquisition",
"slug": "breaking-major-tech-acquisition",
"description": "Breaking news about major technology acquisition",
"html_description": "<p>Breaking news about major technology acquisition</p>",
"category_id": 1,
"type_id": 1,
"tags": "Breaking, News, Acquisition, Technology",
"created_by_id": 1,
"status_id": 1,
"is_draft": false,
"client_id": "550e8400-e29b-41d4-a716-446655440000"
}'
```
**Expected Response:**
```json
{
"success": true,
"messages": ["Article created and published successfully"],
"data": {
"id": 3,
"title": "BREAKING: Major Tech Acquisition",
"status": "published",
"is_publish": true,
"published_at": "2024-01-15T16:05:00Z",
"approval_bypassed": true,
"bypass_reason": "approval_disabled"
}
}
```
### Step 4: Re-enable Approval System
```bash
curl -X PUT "${BASE_URL}/client-approval-settings/1" \
-H "${AUTH_HEADER}" \
-H "${CLIENT_HEADER}" \
-H "${CONTENT_TYPE}" \
-d '{
"requires_approval": true,
"auto_publish_articles": false,
"default_workflow_id": 1,
"reason": "Returning to normal approval process"
}'
```
---
## 📊 Scenario 5: Approval Dashboard and Monitoring
### Step 1: Get Pending Approvals
```bash
curl -X GET "${BASE_URL}/approvals/pending?page=1&limit=10" \
-H "${AUTH_HEADER}" \
-H "${CLIENT_HEADER}"
```
### Step 2: Get My Approval Queue
```bash
curl -X GET "${BASE_URL}/approvals/my-queue?page=1&limit=10" \
-H "${AUTH_HEADER}" \
-H "${CLIENT_HEADER}"
```
### Step 3: Get Approval History for Article
```bash
curl -X GET "${BASE_URL}/articles/1/approval-history" \
-H "${AUTH_HEADER}" \
-H "${CLIENT_HEADER}"
```
### Step 4: Get My Approval Statistics
```bash
curl -X GET "${BASE_URL}/approvals/my-stats" \
-H "${AUTH_HEADER}" \
-H "${CLIENT_HEADER}"
```
---
## 🔧 Scenario 6: Workflow Management
### Step 1: Get All Workflows
```bash
curl -X GET "${BASE_URL}/approval-workflows?page=1&limit=10" \
-H "${AUTH_HEADER}" \
-H "${CLIENT_HEADER}"
```
### Step 2: Get Workflow by ID
```bash
curl -X GET "${BASE_URL}/approval-workflows/1" \
-H "${AUTH_HEADER}" \
-H "${CLIENT_HEADER}"
```
### Step 3: Update Workflow
```bash
curl -X PUT "${BASE_URL}/approval-workflows/1" \
-H "${AUTH_HEADER}" \
-H "${CLIENT_HEADER}" \
-H "${CONTENT_TYPE}" \
-d '{
"name": "Updated 3-Level Editorial Review",
"description": "Updated workflow with improved efficiency",
"is_active": true
}'
```
### Step 4: Add New Workflow Step
```bash
curl -X POST "${BASE_URL}/approval-workflow-steps" \
-H "${AUTH_HEADER}" \
-H "${CLIENT_HEADER}" \
-H "${CONTENT_TYPE}" \
-d '{
"workflow_id": 1,
"step_order": 2,
"step_name": "Legal Review",
"required_user_level_id": 4,
"can_skip": true,
"auto_approve_after_hours": 24,
"client_id": "550e8400-e29b-41d4-a716-446655440000"
}'
```
---
## 🧪 Test Data Setup Script
```bash
#!/bin/bash
# Set environment variables
BASE_URL="http://localhost:8800/api"
AUTH_HEADER="Authorization: Bearer YOUR_JWT_TOKEN"
CLIENT_HEADER="X-Client-Key: YOUR_CLIENT_KEY"
CONTENT_TYPE="Content-Type: application/json"
# Function to make API calls
make_request() {
local method=$1
local endpoint=$2
local data=$3
if [ -n "$data" ]; then
curl -X "$method" "${BASE_URL}${endpoint}" \
-H "$AUTH_HEADER" \
-H "$CLIENT_HEADER" \
-H "$CONTENT_TYPE" \
-d "$data"
else
curl -X "$method" "${BASE_URL}${endpoint}" \
-H "$AUTH_HEADER" \
-H "$CLIENT_HEADER"
fi
}
echo "Setting up test data..."
# 1. Create client
echo "Creating client..."
make_request "POST" "/clients" '{
"name": "Test Media Company",
"is_active": true
}'
# 2. Create user levels
echo "Creating user levels..."
make_request "POST" "/user-levels" '{
"name": "Editor",
"alias_name": "ED",
"level_number": 1,
"is_approval_active": true
}'
make_request "POST" "/user-levels" '{
"name": "Senior Editor",
"alias_name": "SED",
"level_number": 2,
"is_approval_active": true
}'
make_request "POST" "/user-levels" '{
"name": "Editor in Chief",
"alias_name": "EIC",
"level_number": 3,
"is_approval_active": true
}'
# 3. Create approval workflow
echo "Creating approval workflow..."
make_request "POST" "/approval-workflows" '{
"name": "Standard 3-Level Editorial Review",
"description": "Complete editorial workflow with 3 approval levels",
"is_default": true,
"is_active": true,
"requires_approval": true,
"auto_publish": false
}'
echo "Test data setup completed!"
```
---
## 📋 Test Validation Checklist
### ✅ Functional Testing
- [ ] Client creation and configuration
- [ ] User level management
- [ ] Approval workflow creation and modification
- [ ] Article creation and submission
- [ ] Complete approval process flow
- [ ] Article rejection and revision process
- [ ] Dynamic approval toggle functionality
- [ ] Approval dashboard and monitoring
- [ ] Multi-step workflow progression
- [ ] Auto-publish functionality
### ✅ Error Handling
- [ ] Invalid client key handling
- [ ] Invalid JWT token handling
- [ ] Missing required fields validation
- [ ] Workflow step validation
- [ ] User permission validation
- [ ] Article status validation
### ✅ Performance Testing
- [ ] Response time < 500ms for all endpoints
- [ ] Concurrent approval processing
- [ ] Large dataset pagination
- [ ] Database query optimization
### ✅ Security Testing
- [ ] Client isolation
- [ ] User authorization
- [ ] Data validation and sanitization
- [ ] SQL injection prevention
---
## 🚀 Running the Tests
### Prerequisites
1. Ensure the backend server is running on `http://localhost:8800`
2. Obtain valid JWT token for authentication
3. Set up client key for multi-tenant support
4. Database should be clean and ready for testing
### Execution Steps
1. Run the test data setup script
2. Execute each scenario sequentially
3. Validate responses against expected outputs
4. Check database state after each scenario
5. Clean up test data after completion
### Monitoring
- Monitor server logs during testing
- Check database performance metrics
- Validate all audit trails are created
- Ensure proper error handling and logging
---
*This documentation provides comprehensive end-to-end testing scenarios for the approval workflow system. Each scenario includes detailed curl commands and expected responses for complete testing coverage.*

File diff suppressed because it is too large Load Diff

View File

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