feat: update approval steps

This commit is contained in:
hanif salafi 2025-10-02 04:17:11 +07:00
parent fa01056f51
commit e1b11647c5
12 changed files with 1468 additions and 715 deletions

View File

@ -58,8 +58,6 @@ func (_i *ArticleApprovalFlowsRouter) RegisterArticleApprovalFlowsRoutes() {
router.Post("/:id/multi-branch-approve", articleApprovalFlowsController.ProcessMultiBranchApproval)
router.Post("/articles/:articleId/approve", articleApprovalFlowsController.ApproveArticleByFlow)
router.Put("/:id/approve", articleApprovalFlowsController.Approve)
router.Put("/:id/reject", articleApprovalFlowsController.Reject)
router.Put("/:id/request-revision", articleApprovalFlowsController.RequestRevision)
router.Put("/:id/resubmit", articleApprovalFlowsController.Resubmit)
})
}

View File

@ -1,6 +1,7 @@
package controller
import (
"fmt"
"netidhub-saas-be/app/module/article_approval_flows/request"
"netidhub-saas-be/app/module/article_approval_flows/service"
usersRepository "netidhub-saas-be/app/module/users/repository"
@ -26,8 +27,6 @@ type ArticleApprovalFlowsController interface {
Show(c *fiber.Ctx) error
SubmitForApproval(c *fiber.Ctx) error
Approve(c *fiber.Ctx) error
Reject(c *fiber.Ctx) error
RequestRevision(c *fiber.Ctx) error
Resubmit(c *fiber.Ctx) error
GetMyApprovalQueue(c *fiber.Ctx) error
GetPendingApprovals(c *fiber.Ctx) error
@ -237,110 +236,6 @@ func (_i *articleApprovalFlowsController) Approve(c *fiber.Ctx) error {
})
}
// Reject ArticleApprovalFlows
// @Summary Reject article
// @Description API for rejecting article
// @Tags ArticleApprovalFlows
// @Security Bearer
// @Param Authorization header string true "Insert the Authorization"
// @Param Authorization header string false "Insert your access token" default(Bearer <Add access token here>)
// @Param id path int true "ArticleApprovalFlows ID"
// @Param req body request.RejectionRequest true "Rejection data"
// @Success 200 {object} response.Response
// @Failure 400 {object} response.BadRequestError
// @Failure 401 {object} response.UnauthorizedError
// @Failure 500 {object} response.InternalServerError
// @Router /article-approval-flows/{id}/reject [put]
func (_i *articleApprovalFlowsController) Reject(c *fiber.Ctx) error {
id, err := strconv.Atoi(c.Params("id"))
if err != nil {
return utilRes.ErrorBadRequest(c, "Invalid ID format")
}
req := new(request.RejectionRequest)
if err := c.BodyParser(req); err != nil {
return utilRes.ErrorBadRequest(c, "Invalid request body")
}
if err := utilVal.ParseAndValidate(c, req); err != nil {
return err
}
// Get Authorization token from header
authToken := c.Get("Authorization")
if authToken == "" {
return utilRes.ErrorBadRequest(c, "Authorization token required")
}
user := utilSvc.GetUserInfo(_i.Log, _i.UsersRepo, authToken)
if user == nil {
return utilRes.ErrorBadRequest(c, "Invalid authorization token")
}
err = _i.articleApprovalFlowsService.RejectArticle(authToken, uint(id), user.ID, req.Reason)
if err != nil {
return err
}
return utilRes.Resp(c, utilRes.Response{
Success: true,
Messages: utilRes.Messages{"Article successfully rejected"},
Data: nil,
})
}
// RequestRevision ArticleApprovalFlows
// @Summary Request revision for article
// @Description API for requesting revision for article
// @Tags ArticleApprovalFlows
// @Security Bearer
// @Param Authorization header string true "Insert the Authorization"
// @Param Authorization header string false "Insert your access token" default(Bearer <Add access token here>)
// @Param id path int true "ArticleApprovalFlows ID"
// @Param req body request.RevisionRequest true "Revision request data"
// @Success 200 {object} response.Response
// @Failure 400 {object} response.BadRequestError
// @Failure 401 {object} response.UnauthorizedError
// @Failure 500 {object} response.InternalServerError
// @Router /article-approval-flows/{id}/request-revision [put]
func (_i *articleApprovalFlowsController) RequestRevision(c *fiber.Ctx) error {
id, err := strconv.Atoi(c.Params("id"))
if err != nil {
return utilRes.ErrorBadRequest(c, "Invalid ID format")
}
req := new(request.RevisionRequest)
if err := c.BodyParser(req); err != nil {
return utilRes.ErrorBadRequest(c, "Invalid request body")
}
if err := utilVal.ParseAndValidate(c, req); err != nil {
return err
}
// Get Authorization token from header
authToken := c.Get("Authorization")
if authToken == "" {
return utilRes.ErrorBadRequest(c, "Authorization token required")
}
user := utilSvc.GetUserInfo(_i.Log, _i.UsersRepo, authToken)
if user == nil {
return utilRes.ErrorBadRequest(c, "Invalid authorization token")
}
err = _i.articleApprovalFlowsService.RequestRevision(authToken, uint(id), user.ID, req.Message)
if err != nil {
return err
}
return utilRes.Resp(c, utilRes.Response{
Success: true,
Messages: utilRes.Messages{"Revision successfully requested"},
Data: nil,
})
}
// Resubmit ArticleApprovalFlows
// @Summary Resubmit article after revision
// @Description API for resubmitting article after revision
@ -801,17 +696,18 @@ func (_i *articleApprovalFlowsController) ApproveArticleByFlow(c *fiber.Ctx) err
}
// Process approval using multi-branch logic
err = _i.articleApprovalFlowsService.ProcessMultiBranchApproval(authToken, activeFlow.ID, user.ID, req.Message)
err = _i.articleApprovalFlowsService.ProcessApprovalAction(authToken, activeFlow.ID, req.Action, user.ID, req.Message)
if err != nil {
return err
}
return utilRes.Resp(c, utilRes.Response{
Success: true,
Messages: utilRes.Messages{"Article successfully approved through active approval flow"},
Messages: utilRes.Messages{fmt.Sprintf("Article successfully %s through active approval flow", req.Action)},
Data: map[string]interface{}{
"article_id": articleId,
"flow_id": activeFlow.ID,
"action": req.Action,
"current_step": activeFlow.CurrentStep,
"current_branch": activeFlow.CurrentBranch,
"workflow_id": activeFlow.WorkflowId,

View File

@ -27,6 +27,7 @@ type SubmitForApprovalRequest struct {
}
type ApprovalActionRequest struct {
Action string `json:"action" validate:"required,oneof=approve revision reject"`
Message string `json:"message"`
}

View File

@ -3,6 +3,7 @@ package service
import (
"encoding/json"
"errors"
"fmt"
"netidhub-saas-be/app/database/entity"
approvalWorkflowStepsRepo "netidhub-saas-be/app/module/approval_workflow_steps/repository"
approvalWorkflowsRepo "netidhub-saas-be/app/module/approval_workflows/repository"
@ -41,8 +42,6 @@ type ArticleApprovalFlowsService interface {
// Article submission and approval workflow
SubmitArticleForApproval(authToken string, articleId uint, submittedById uint, workflowId *uint) (flow *entity.ArticleApprovalFlows, err error)
ApproveStep(authToken string, flowId uint, approvedById uint, message string) (err error)
RejectArticle(authToken string, flowId uint, rejectedById uint, reason string) (err error)
RequestRevision(authToken string, flowId uint, requestedById uint, revisionMessage string) (err error)
ResubmitAfterRevision(authToken string, flowId uint, resubmittedById uint) (err error)
// Dashboard and queue methods
@ -63,6 +62,7 @@ type ArticleApprovalFlowsService interface {
// Multi-branch support methods
ProcessMultiBranchApproval(authToken string, flowId uint, approvedById uint, message string) (err error)
ProcessApprovalAction(authToken string, flowId uint, action string, actionById uint, message string) (err error)
FindNextStepsForBranch(authToken string, workflowId uint, currentStep int, submitterLevelId uint) (steps []*entity.ApprovalWorkflowSteps, err error)
IsStepApplicableForLevel(step *entity.ApprovalWorkflowSteps, submitterLevelId uint) (isApplicable bool, err error)
ProcessParallelBranches(authToken string, flow *entity.ArticleApprovalFlows, nextSteps []*entity.ApprovalWorkflowSteps, approvedById uint, message string) (err error)
@ -567,169 +567,6 @@ func (_i *articleApprovalFlowsService) ApproveStep(authToken string, flowId uint
return nil
}
func (_i *articleApprovalFlowsService) RejectArticle(authToken string, flowId uint, rejectedById uint, reason string) (err error) {
// Extract clientId from authToken
var clientId *uuid.UUID
if authToken != "" {
user := utilSvc.GetUserInfo(_i.Log, _i.UsersRepository, authToken)
if user != nil && user.ClientId != nil {
clientId = user.ClientId
_i.Log.Info().Interface("clientId", clientId).Msg("Extracted clientId from auth token")
}
}
if clientId == nil {
return errors.New("clientId not found in auth token")
}
// Get approval flow
flow, err := _i.ArticleApprovalFlowsRepository.FindOne(clientId, flowId)
if err != nil {
return err
}
if flow == nil {
return errors.New("approval flow not found")
}
if flow.StatusId != 1 && flow.StatusId != 4 { // not pending or revision_requested
return errors.New("approval flow is not in pending state")
}
// Get current step
currentStep, err := _i.ApprovalWorkflowStepsRepository.FindByWorkflowAndStep(clientId, flow.WorkflowId, flow.CurrentStep)
if err != nil {
return err
}
// Create step log
stepLog := &entity.ArticleApprovalStepLogs{
ApprovalFlowId: flow.ID,
StepOrder: flow.CurrentStep,
StepName: currentStep.StepName,
ApprovedById: &rejectedById,
Action: "reject",
Message: &reason,
ProcessedAt: time.Now(),
UserLevelId: currentStep.RequiredUserLevelId,
}
_, err = _i.ArticleApprovalStepLogsRepository.Create(clientId, stepLog)
if err != nil {
return err
}
// Update approval flow status
flowUpdate := &entity.ArticleApprovalFlows{
StatusId: 3, // rejected
RejectionReason: &reason,
CompletedAt: &[]time.Time{time.Now()}[0],
}
err = _i.ArticleApprovalFlowsRepository.Update(flowId, flowUpdate)
if err != nil {
return err
}
// Get current article data first
currentArticle, err := _i.ArticlesRepository.FindOne(clientId, flow.ArticleId)
if err != nil {
return err
}
// Update only the necessary fields
currentArticle.StatusId = &[]int{3}[0] // rejected
currentArticle.CurrentApprovalStep = nil
err = _i.ArticlesRepository.UpdateSkipNull(clientId, flow.ArticleId, currentArticle)
if err != nil {
return err
}
return nil
}
func (_i *articleApprovalFlowsService) RequestRevision(authToken string, flowId uint, requestedById uint, revisionMessage string) (err error) {
// Extract clientId from authToken
var clientId *uuid.UUID
if authToken != "" {
user := utilSvc.GetUserInfo(_i.Log, _i.UsersRepository, authToken)
if user != nil && user.ClientId != nil {
clientId = user.ClientId
_i.Log.Info().Interface("clientId", clientId).Msg("Extracted clientId from auth token")
}
}
if clientId == nil {
return errors.New("clientId not found in auth token")
}
// Get approval flow
flow, err := _i.ArticleApprovalFlowsRepository.FindOne(clientId, flowId)
if err != nil {
return err
}
if flow == nil {
return errors.New("approval flow not found")
}
if flow.StatusId != 1 { // not pending
return errors.New("approval flow is not in pending state")
}
// Get current step
currentStep, err := _i.ApprovalWorkflowStepsRepository.FindByWorkflowAndStep(clientId, flow.WorkflowId, flow.CurrentStep)
if err != nil {
return err
}
// Create step log
stepLog := &entity.ArticleApprovalStepLogs{
ApprovalFlowId: flow.ID,
StepOrder: flow.CurrentStep,
StepName: currentStep.StepName,
ApprovedById: &requestedById,
Action: "request_revision",
Message: &revisionMessage,
ProcessedAt: time.Now(),
UserLevelId: currentStep.RequiredUserLevelId,
}
_, err = _i.ArticleApprovalStepLogsRepository.Create(clientId, stepLog)
if err != nil {
return err
}
// Update approval flow status
flowUpdate := &entity.ArticleApprovalFlows{
StatusId: 4, // revision_requested
RevisionRequested: &[]bool{true}[0],
RevisionMessage: &revisionMessage,
}
err = _i.ArticleApprovalFlowsRepository.Update(flowId, flowUpdate)
if err != nil {
return err
}
// Get current article data first
currentArticle, err := _i.ArticlesRepository.FindOne(clientId, flow.ArticleId)
if err != nil {
return err
}
// Update only the necessary fields
currentArticle.StatusId = &[]int{4}[0] // revision_requested
err = _i.ArticlesRepository.UpdateSkipNull(clientId, flow.ArticleId, currentArticle)
if err != nil {
return err
}
return nil
}
func (_i *articleApprovalFlowsService) ResubmitAfterRevision(authToken string, flowId uint, resubmittedById uint) (err error) {
// Extract clientId from authToken
var clientId *uuid.UUID
@ -1382,3 +1219,166 @@ func (_i *articleApprovalFlowsService) GetUserLevelId(authToken string, userId u
func (_i *articleApprovalFlowsService) FindActiveByArticleId(articleId uint) (flow *entity.ArticleApprovalFlows, err error) {
return _i.ArticleApprovalFlowsRepository.FindActiveByArticleId(articleId)
}
// ProcessApprovalAction processes different approval actions (approve, request_update, reject)
func (_i *articleApprovalFlowsService) ProcessApprovalAction(authToken string, flowId uint, action string, actionById uint, message string) (err error) {
_i.Log.Info().
Uint("flowId", flowId).
Str("action", action).
Uint("actionById", actionById).
Str("message", message).
Msg("Processing approval action")
// Extract clientId from authToken
var clientId *uuid.UUID
if authToken != "" {
user := utilSvc.GetUserInfo(_i.Log, _i.UsersRepository, authToken)
if user != nil && user.ClientId != nil {
clientId = user.ClientId
}
}
// Get the approval flow
flow, err := _i.ArticleApprovalFlowsRepository.FindOne(clientId, flowId)
if err != nil {
return fmt.Errorf("failed to find approval flow: %w", err)
}
// Get the article
article, err := _i.ArticlesRepository.FindOne(clientId, flow.ArticleId)
if err != nil {
return fmt.Errorf("failed to find article: %w", err)
}
switch action {
case "approve":
return _i.processApproveAction(authToken, flow, article, actionById, message)
case "revision":
return _i.processRequestUpdateAction(authToken, flow, article, actionById, message)
case "reject":
return _i.processRejectAction(authToken, flow, article, actionById, message)
default:
return fmt.Errorf("invalid action: %s", action)
}
}
// processApproveAction handles approve action - moves to next step
func (_i *articleApprovalFlowsService) processApproveAction(authToken string, flow *entity.ArticleApprovalFlows, article *entity.Articles, actionById uint, message string) error {
_i.Log.Info().Uint("flowId", flow.ID).Msg("Processing approve action")
// Use existing multi-branch approval logic
return _i.ProcessMultiBranchApproval(authToken, flow.ID, actionById, message)
}
// processRequestUpdateAction handles request update action - moves back 1 step
func (_i *articleApprovalFlowsService) processRequestUpdateAction(authToken string, flow *entity.ArticleApprovalFlows, article *entity.Articles, actionById uint, message string) error {
_i.Log.Info().Uint("flowId", flow.ID).Msg("Processing request update action")
var clientId *uuid.UUID
if authToken != "" {
user := utilSvc.GetUserInfo(_i.Log, _i.UsersRepository, authToken)
if user != nil && user.ClientId != nil {
clientId = user.ClientId
}
}
// Reset to initial step (step 1) and status pending
flow.CurrentStep = 1
flow.StatusId = 1 // pending - seperti belum berjalan flow approval
flow.RevisionRequested = &[]bool{true}[0]
flow.RevisionMessage = &message
err := _i.ArticleApprovalFlowsRepository.Update(flow.ID, flow)
if err != nil {
return fmt.Errorf("failed to update flow for request update: %w", err)
}
// Update article status to draft for revision
article.StatusId = &[]int{1}[0] // 1 = draft
article.IsDraft = &[]bool{true}[0]
article.WorkflowId = &flow.WorkflowId // Keep workflow ID for restart
article.CurrentApprovalStep = &[]int{1}[0] // Reset to step 1
err = _i.ArticlesRepository.Update(clientId, article.ID, article)
if err != nil {
return fmt.Errorf("failed to update article for revision: %w", err)
}
// Create step log
stepLog := &entity.ArticleApprovalStepLogs{
ApprovalFlowId: flow.ID,
StepOrder: flow.CurrentStep,
Action: "revision",
ApprovedById: &actionById,
Message: &message,
CreatedAt: time.Now(),
}
_, err = _i.ArticleApprovalStepLogsRepository.Create(clientId, stepLog)
if err != nil {
_i.Log.Error().Err(err).Msg("Failed to create step log for request update")
}
_i.Log.Info().
Uint("flowId", flow.ID).
Int("resetStep", 1).
Msg("Revision request processed - flow reset to step 1")
return nil
}
// processRejectAction handles reject action - returns to draft status
func (_i *articleApprovalFlowsService) processRejectAction(authToken string, flow *entity.ArticleApprovalFlows, article *entity.Articles, actionById uint, message string) error {
_i.Log.Info().Uint("flowId", flow.ID).Msg("Processing reject action")
var clientId *uuid.UUID
if authToken != "" {
user := utilSvc.GetUserInfo(_i.Log, _i.UsersRepository, authToken)
if user != nil && user.ClientId != nil {
clientId = user.ClientId
}
}
// Reset to initial step (step 1) and status pending
flow.CurrentStep = 1
flow.StatusId = 1 // pending - seperti belum berjalan flow approval
flow.RevisionMessage = &message // Use same field for consistency
// Don't set completedAt - flow is not completed, just reset
err := _i.ArticleApprovalFlowsRepository.Update(flow.ID, flow)
if err != nil {
return fmt.Errorf("failed to update flow for rejection: %w", err)
}
// Update article status to draft
article.StatusId = &[]int{1}[0] // 1 = draft
article.IsDraft = &[]bool{true}[0]
article.WorkflowId = &flow.WorkflowId // Keep workflow ID for restart
article.CurrentApprovalStep = &[]int{1}[0] // Reset to step 1
err = _i.ArticlesRepository.Update(clientId, article.ID, article)
if err != nil {
return fmt.Errorf("failed to update article for rejection: %w", err)
}
// Create step log
stepLog := &entity.ArticleApprovalStepLogs{
ApprovalFlowId: flow.ID,
StepOrder: flow.CurrentStep,
Action: "reject",
ApprovedById: &actionById,
Message: &message,
CreatedAt: time.Now(),
}
_, err = _i.ArticleApprovalStepLogsRepository.Create(clientId, stepLog)
if err != nil {
_i.Log.Error().Err(err).Msg("Failed to create step log for rejection")
}
_i.Log.Info().
Uint("flowId", flow.ID).
Uint("articleId", article.ID).
Msg("Rejection processed - flow reset to step 1, article returned to draft")
return nil
}

View File

@ -233,6 +233,7 @@ func (_i *articlesController) SaveThumbnail(c *fiber.Ctx) error {
// @Tags Articles
// @Security Bearer
// @Param Authorization header string false "Insert your access token" default(Bearer <Add access token here>)
// @Param payload body request.ArticlesUpdateRequest true "Required payload"
// @Param id path int true "Articles ID"
// @Success 200 {object} response.Response
// @Failure 400 {object} response.BadRequestError

View File

@ -0,0 +1,286 @@
# Enhanced Approval Actions API
## 🚀 **Overview**
API ini telah ditingkatkan untuk mendukung 3 jenis action approval yang berbeda: **Approve**, **Request Update**, dan **Reject**. Setiap action memiliki behavior yang berbeda sesuai dengan kebutuhan workflow.
## 📋 **Endpoint**
```
POST /api/article-approval-flows/articles/{articleId}/approve
```
## 🔧 **Request Body**
```json
{
"action": "approve",
"message": "Content looks good, approved for next level"
}
```
## 📝 **Action Types**
### **1. Approve (`approve`)**
- **Behavior**: Naik ke step berikutnya dalam workflow
- **Use Case**: Content sudah sesuai dan siap untuk step selanjutnya
- **Result**: Article akan diproses menggunakan multi-branch logic
### **2. Revision (`revision`)**
- **Behavior**: Mundur 1 step dalam workflow
- **Use Case**: Content perlu diperbaiki tapi masih bisa diperbaiki
- **Result**: Article akan kembali ke step sebelumnya untuk revisi
### **3. Reject (`reject`)**
- **Behavior**: Balik ke status Draft
- **Use Case**: Content tidak sesuai dan perlu dibuat ulang
- **Result**: Article akan dikembalikan ke status draft dan workflow dihentikan
## 📤 **Response Examples**
### **Approve Response (200):**
```json
{
"success": true,
"messages": ["Article successfully approve through active approval flow"],
"data": {
"article_id": 123,
"flow_id": 456,
"action": "approve",
"current_step": 2,
"current_branch": "Branch_A",
"workflow_id": 1
}
}
```
### **Revision Response (200):**
```json
{
"success": true,
"messages": ["Article successfully revision through active approval flow"],
"data": {
"article_id": 123,
"flow_id": 456,
"action": "revision",
"current_step": 1,
"current_branch": "Branch_A",
"workflow_id": 1
}
}
```
### **Reject Response (200):**
```json
{
"success": true,
"messages": ["Article successfully reject through active approval flow"],
"data": {
"article_id": 123,
"flow_id": 456,
"action": "reject",
"current_step": 1,
"current_branch": "Branch_A",
"workflow_id": 1
}
}
```
## 🧪 **Curl Examples**
### **1. Approve Article:**
```bash
curl -X POST "http://localhost:8080/api/article-approval-flows/articles/123/approve" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer YOUR_TOKEN" \
-d '{
"action": "approve",
"message": "Content reviewed and approved. Ready for next level."
}'
```
### **2. Request Revision:**
```bash
curl -X POST "http://localhost:8080/api/article-approval-flows/articles/123/approve" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer YOUR_TOKEN" \
-d '{
"action": "revision",
"message": "Please fix the grammar and improve the conclusion section."
}'
```
### **3. Reject Article:**
```bash
curl -X POST "http://localhost:8080/api/article-approval-flows/articles/123/approve" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer YOUR_TOKEN" \
-d '{
"action": "reject",
"message": "Content does not meet our quality standards. Please rewrite the entire article."
}'
```
## 🔄 **Workflow Behavior**
### **Approve Flow:**
1. **Input**: `{"action": "approve", "message": "..."}`
2. **Process**: Menggunakan multi-branch approval logic
3. **Result**: Article naik ke step berikutnya
4. **Status**: Workflow berlanjut
### **Revision Flow:**
1. **Input**: `{"action": "revision", "message": "..."}`
2. **Process**:
- Current step dikurangi 1
- `revision_requested` = true
- `revision_message` = message
3. **Result**: Article kembali ke step sebelumnya
4. **Status**: Workflow berlanjut tapi mundur 1 step
### **Reject Flow:**
1. **Input**: `{"action": "reject", "message": "..."}`
2. **Process**:
- Flow status = rejected (3)
- Article status = draft (1)
- `is_draft` = true
- `workflow_id` = null
- `current_approval_step` = null
3. **Result**: Article dikembalikan ke draft
4. **Status**: Workflow dihentikan
## 📊 **Database Changes**
### **Article Approval Flows Table:**
- `revision_requested`: Boolean flag untuk request update
- `revision_message`: Pesan untuk revisi
- `rejection_reason`: Alasan penolakan
- `completed_at`: Timestamp ketika workflow selesai
### **Articles Table:**
- `status_id`: 1 (draft), 2 (published), 3 (rejected)
- `is_draft`: Boolean flag untuk draft status
- `workflow_id`: Null ketika di-reject
- `current_approval_step`: Null ketika di-reject
### **Article Approval Step Logs Table:**
- `action`: "approve", "request_update", "reject"
- `message`: Pesan dari approver
- `action_by_id`: ID user yang melakukan action
## 🎯 **Use Cases**
### **1. Content Review Process:**
```bash
# Step 1: Editor review
curl -X POST "http://localhost:8080/api/article-approval-flows/articles/123/approve" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer EDITOR_TOKEN" \
-d '{"action": "approve", "message": "Content is good"}'
# Step 2: Senior Editor review
curl -X POST "http://localhost:8080/api/article-approval-flows/articles/123/approve" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer SENIOR_EDITOR_TOKEN" \
-d '{"action": "request_update", "message": "Please add more examples"}'
# Step 3: After revision, approve again
curl -X POST "http://localhost:8080/api/article-approval-flows/articles/123/approve" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer SENIOR_EDITOR_TOKEN" \
-d '{"action": "approve", "message": "Much better now"}'
```
### **2. Quality Control Process:**
```bash
# Quality check fails
curl -X POST "http://localhost:8080/api/article-approval-flows/articles/123/approve" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer QUALITY_CHECKER_TOKEN" \
-d '{"action": "reject", "message": "Content does not meet quality standards"}'
```
## 🔒 **Validation Rules**
### **Action Validation:**
- `action` harus salah satu dari: `approve`, `request_update`, `reject`
- `action` adalah field required
### **Message Validation:**
- `message` adalah optional
- Jika ada, akan disimpan dalam step log
### **Authorization Validation:**
- User harus memiliki permission untuk approve
- User level harus sesuai dengan step yang sedang berjalan
## 🚨 **Error Handling**
### **Invalid Action (400):**
```json
{
"success": false,
"messages": ["Validation failed: [Action must be one of: approve, request_update, reject]"]
}
```
### **No Active Flow (400):**
```json
{
"success": false,
"messages": ["No active approval flow found for this article"]
}
```
### **Unauthorized (401):**
```json
{
"success": false,
"messages": ["Invalid authorization token"]
}
```
## 🔄 **Migration from Old API**
### **Before (Only Approve):**
```bash
curl -X POST "http://localhost:8080/api/article-approval-flows/articles/123/approve" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer YOUR_TOKEN" \
-d '{"message": "Approved"}'
```
### **After (3 Actions):**
```bash
# Approve
curl -X POST "http://localhost:8080/api/article-approval-flows/articles/123/approve" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer YOUR_TOKEN" \
-d '{"action": "approve", "message": "Approved"}'
# Revision
curl -X POST "http://localhost:8080/api/article-approval-flows/articles/123/approve" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer YOUR_TOKEN" \
-d '{"action": "revision", "message": "Please fix grammar"}'
# Reject
curl -X POST "http://localhost:8080/api/article-approval-flows/articles/123/approve" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer YOUR_TOKEN" \
-d '{"action": "reject", "message": "Content not suitable"}'
```
## 🎉 **Benefits**
1. **Flexible Workflow**: Support untuk berbagai jenis feedback
2. **Better User Experience**: User bisa memberikan feedback yang lebih spesifik
3. **Improved Quality Control**: Reject option untuk content yang tidak sesuai
4. **Revision Support**: Revision untuk perbaikan tanpa menghentikan workflow
5. **Audit Trail**: Semua action dicatat dalam step logs
---
**🎉 Enhanced Approval Actions API memberikan kontrol yang lebih baik atas workflow approval dengan 3 pilihan action yang berbeda!**
Sekarang approver bisa memilih apakah akan approve, revision, atau reject artikel dengan behavior yang sesuai untuk setiap pilihan. 🚀

View File

@ -0,0 +1,423 @@
# Enhanced Approval Actions - Practical Usage Guide
## 🎯 **Overview**
Panduan praktis untuk menggunakan Enhanced Approval Actions API yang mendukung 3 jenis action: **Approve**, **Revision**, dan **Reject**.
## 🔄 **Action Types & Behavior**
| Action | Behavior | Use Case | Result |
|--------|----------|----------|---------|
| **`approve`** | Naik ke step berikutnya | Content sudah sesuai | Workflow berlanjut |
| **`revision`** | Mundur 1 step | Content perlu diperbaiki | Workflow mundur 1 step |
| **`reject`** | Balik ke Draft | Content tidak sesuai | Workflow dihentikan, artikel jadi draft |
## 🧪 **Practical Examples**
### **Scenario 1: Normal Approval Flow**
#### **Step 1: Editor Review**
```bash
curl -X POST "http://localhost:8080/api/article-approval-flows/articles/10/approve" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer EDITOR_TOKEN" \
-d '{
"action": "approve",
"message": "Content is well-written and follows our guidelines"
}'
```
**Response:**
```json
{
"success": true,
"messages": ["Article successfully approve through active approval flow"],
"data": {
"article_id": 10,
"flow_id": 1,
"action": "approve",
"current_step": 2,
"current_branch": "Final_Approval",
"workflow_id": 1
}
}
```
#### **Step 2: Senior Editor Review**
```bash
curl -X POST "http://localhost:8080/api/article-approval-flows/articles/10/approve" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer SENIOR_EDITOR_TOKEN" \
-d '{
"action": "approve",
"message": "Final approval granted. Ready for publication."
}'
```
**Result:** Article published successfully! 🎉
---
### **Scenario 2: Request Update Flow**
#### **Step 1: Editor Review**
```bash
curl -X POST "http://localhost:8080/api/article-approval-flows/articles/11/approve" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer EDITOR_TOKEN" \
-d '{
"action": "approve",
"message": "Content is good, moving to next level"
}'
```
#### **Step 2: Senior Editor Review (Request Revision)**
```bash
curl -X POST "http://localhost:8080/api/article-approval-flows/articles/11/approve" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer SENIOR_EDITOR_TOKEN" \
-d '{
"action": "revision",
"message": "Please add more examples in section 3 and improve the conclusion"
}'
```
**Response:**
```json
{
"success": true,
"messages": ["Article successfully revision through active approval flow"],
"data": {
"article_id": 11,
"flow_id": 2,
"action": "revision",
"current_step": 1,
"current_branch": "Branch_A",
"workflow_id": 1
}
}
```
**Result:** Article kembali ke step 1 untuk revisi! 📝
#### **Step 3: After Revision, Approve Again**
```bash
curl -X POST "http://localhost:8080/api/article-approval-flows/articles/11/approve" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer EDITOR_TOKEN" \
-d '{
"action": "approve",
"message": "Revisions completed, content improved significantly"
}'
```
---
### **Scenario 3: Reject Flow**
#### **Step 1: Editor Review (Reject)**
```bash
curl -X POST "http://localhost:8080/api/article-approval-flows/articles/12/approve" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer EDITOR_TOKEN" \
-d '{
"action": "reject",
"message": "Content does not meet our quality standards. Please rewrite with better research and clearer structure."
}'
```
**Response:**
```json
{
"success": true,
"messages": ["Article successfully reject through active approval flow"],
"data": {
"article_id": 12,
"flow_id": 3,
"action": "reject",
"current_step": 1,
"current_branch": "Branch_A",
"workflow_id": 1
}
}
```
**Result:** Article dikembalikan ke status Draft! ❌
---
## 🔍 **Database State Changes**
### **After Approve:**
```sql
-- Article Approval Flows
UPDATE article_approval_flows
SET current_step = 2,
updated_at = NOW()
WHERE id = 1;
-- Article Approval Step Logs
INSERT INTO article_approval_step_logs
(approval_flow_id, step_order, action, action_by_id, message, created_at)
VALUES (1, 1, 'approve', 5, 'Content is well-written', NOW());
```
### **After Revision:**
```sql
-- Article Approval Flows
UPDATE article_approval_flows
SET current_step = 1,
revision_requested = true,
revision_message = 'Please add more examples',
updated_at = NOW()
WHERE id = 2;
-- Article Approval Step Logs
INSERT INTO article_approval_step_logs
(approval_flow_id, step_order, action, action_by_id, message, created_at)
VALUES (2, 2, 'revision', 6, 'Please add more examples', NOW());
```
### **After Reject:**
```sql
-- Article Approval Flows
UPDATE article_approval_flows
SET status_id = 3,
rejection_reason = 'Content does not meet quality standards',
completed_at = NOW(),
updated_at = NOW()
WHERE id = 3;
-- Articles
UPDATE articles
SET status_id = 1,
is_draft = true,
workflow_id = NULL,
current_approval_step = NULL,
updated_at = NOW()
WHERE id = 12;
-- Article Approval Step Logs
INSERT INTO article_approval_step_logs
(approval_flow_id, step_order, action, action_by_id, message, created_at)
VALUES (3, 1, 'reject', 5, 'Content does not meet quality standards', NOW());
```
## 🎯 **Frontend Integration Examples**
### **React Component:**
```jsx
import React, { useState } from 'react';
const ApprovalActions = ({ articleId, onActionComplete }) => {
const [action, setAction] = useState('approve');
const [message, setMessage] = useState('');
const handleSubmit = async () => {
try {
const response = await fetch(`/api/article-approval-flows/articles/${articleId}/approve`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
},
body: JSON.stringify({ action, message })
});
const result = await response.json();
if (result.success) {
onActionComplete(result.data);
alert(`Article ${action} successfully!`);
} else {
alert(`Error: ${result.messages[0]}`);
}
} catch (error) {
alert('Failed to process approval action');
}
};
return (
<div className="approval-actions">
<h3>Approval Actions</h3>
<div className="action-buttons">
<button
className={action === 'approve' ? 'active' : ''}
onClick={() => setAction('approve')}
>
✅ Approve
</button>
<button
className={action === 'revision' ? 'active' : ''}
onClick={() => setAction('revision')}
>
📝 Revision
</button>
<button
className={action === 'reject' ? 'active' : ''}
onClick={() => setAction('reject')}
>
❌ Reject
</button>
</div>
<textarea
placeholder="Enter your message..."
value={message}
onChange={(e) => setMessage(e.target.value)}
/>
<button onClick={handleSubmit}>
Submit {action}
</button>
</div>
);
};
```
### **Vue.js Component:**
```vue
<template>
<div class="approval-actions">
<h3>Approval Actions</h3>
<div class="action-buttons">
<button
v-for="actionType in actions"
:key="actionType.value"
:class="{ active: selectedAction === actionType.value }"
@click="selectedAction = actionType.value"
>
{{ actionType.icon }} {{ actionType.label }}
</button>
</div>
<textarea
v-model="message"
placeholder="Enter your message..."
/>
<button @click="submitAction" :disabled="!selectedAction">
Submit {{ selectedAction }}
</button>
</div>
</template>
<script>
export default {
data() {
return {
selectedAction: 'approve',
message: '',
actions: [
{ value: 'approve', label: 'Approve', icon: '✅' },
{ value: 'revision', label: 'Revision', icon: '📝' },
{ value: 'reject', label: 'Reject', icon: '❌' }
]
}
},
methods: {
async submitAction() {
try {
const response = await fetch(`/api/article-approval-flows/articles/${this.articleId}/approve`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${this.token}`
},
body: JSON.stringify({
action: this.selectedAction,
message: this.message
})
});
const result = await response.json();
if (result.success) {
this.$emit('action-complete', result.data);
alert(`Article ${this.selectedAction} successfully!`);
} else {
alert(`Error: ${result.messages[0]}`);
}
} catch (error) {
alert('Failed to process approval action');
}
}
}
}
</script>
```
## 🚨 **Error Handling Examples**
### **Invalid Action:**
```bash
curl -X POST "http://localhost:8080/api/article-approval-flows/articles/123/approve" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer YOUR_TOKEN" \
-d '{
"action": "invalid_action",
"message": "Test"
}'
```
**Response:**
```json
{
"success": false,
"messages": ["Validation failed: [Action must be one of: approve, revision, reject]"]
}
```
### **Missing Action:**
```bash
curl -X POST "http://localhost:8080/api/article-approval-flows/articles/123/approve" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer YOUR_TOKEN" \
-d '{
"message": "Test"
}'
```
**Response:**
```json
{
"success": false,
"messages": ["Validation failed: [Action is required]"]
}
```
## 📊 **Testing Scenarios**
### **Test Case 1: Complete Approval Flow**
1. Create article dengan user level 5
2. Editor (level 3) approve → step 2
3. Senior Editor (level 2) approve → published
### **Test Case 2: Revision Flow**
1. Create article dengan user level 5
2. Editor (level 3) approve → step 2
3. Senior Editor (level 2) revision → step 1
4. Editor (level 3) approve again → step 2
5. Senior Editor (level 2) approve → published
### **Test Case 3: Reject Flow**
1. Create article dengan user level 5
2. Editor (level 3) reject → article jadi draft
3. Workflow dihentikan
## 🎉 **Summary**
Enhanced Approval Actions API memberikan:
- ✅ **3 Action Types**: Approve, Revision, Reject
- ✅ **Flexible Workflow**: Support untuk berbagai skenario
- ✅ **Better UX**: User bisa memberikan feedback yang spesifik
- ✅ **Quality Control**: Reject option untuk content yang tidak sesuai
- ✅ **Revision Support**: Revision untuk perbaikan tanpa menghentikan workflow
- ✅ **Audit Trail**: Semua action dicatat dalam logs
**Sekarang approver memiliki kontrol penuh atas workflow dengan 3 pilihan action yang berbeda!** 🚀

View File

@ -0,0 +1,195 @@
# Refactoring: Unified Approval Actions Implementation
## 🎯 **Overview**
Refactoring ini dilakukan untuk menyelaraskan implementasi approval actions dan menghilangkan duplikasi field message. Sekarang semua action menggunakan field yang sama dan dibedakan hanya berdasarkan `statusId`.
## 🔄 **Perubahan yang Dilakukan**
### **1. Service Layer Changes**
#### **Method yang Dihapus:**
- ✅ `RejectArticle(authToken string, flowId uint, rejectedById uint, reason string)`
- ✅ `RequestRevision(authToken string, flowId uint, requestedById uint, revisionMessage string)`
#### **Method yang Dipertahankan:**
- ✅ `ProcessApprovalAction(authToken string, flowId uint, action string, actionById uint, message string)`
- ✅ `processApproveAction()` - untuk action `approve`
- ✅ `processRequestUpdateAction()` - untuk action `revision`
- ✅ `processRejectAction()` - untuk action `reject`
### **2. Controller Layer Changes**
#### **Method yang Dihapus:**
- ✅ `Reject(c *fiber.Ctx) error`
- ✅ `RequestRevision(c *fiber.Ctx) error`
#### **Method yang Dipertahankan:**
- ✅ `ApproveArticleByFlow(c *fiber.Ctx) error` - menggunakan `ProcessApprovalAction`
### **3. Route Changes**
#### **Route yang Dihapus:**
- ✅ `PUT /article-approval-flows/{id}/reject`
- ✅ `PUT /article-approval-flows/{id}/request-revision`
#### **Route yang Dipertahankan:**
- ✅ `POST /article-approval-flows/articles/{articleId}/approve` - dengan action parameter
## 📊 **Field Unification**
### **Sebelum (Duplikasi Field):**
```go
// Untuk reject
flow.RejectionReason = &message
// Untuk revision
flow.RevisionMessage = &message
```
### **Sesudah (Field Unified):**
```go
// Untuk semua action
flow.RevisionMessage = &message
// Dibedakan berdasarkan statusId
switch action {
case "approve":
// StatusId tetap atau naik step
case "revision":
flow.StatusId = 4 // revision_requested
case "reject":
flow.StatusId = 3 // rejected
}
```
## 🎯 **Action Types & Status Mapping**
| Action | StatusId | Field Used | Behavior |
|--------|----------|------------|----------|
| **`approve`** | 1 atau 2 | `RevisionMessage` | Naik ke step berikutnya |
| **`revision`** | 4 | `RevisionMessage` | Mundur 1 step |
| **`reject`** | 3 | `RevisionMessage` | Balik ke draft |
## 🔧 **Database Field Usage**
### **Unified Field:**
- **`revision_message`**: Digunakan untuk menyimpan pesan dari semua action types
- **`revision_requested`**: Boolean flag untuk revision (hanya untuk action `revision`)
- **`status_id`**: Pembeda utama antara approve/revision/reject
### **Status ID Values:**
- **1**: `pending` - Menunggu approval
- **2**: `approved` - Disetujui
- **3**: `rejected` - Ditolak
- **4**: `revision_requested` - Diminta revisi
## 🚀 **API Usage**
### **Single Endpoint untuk Semua Actions:**
```bash
POST /api/article-approval-flows/articles/{articleId}/approve
```
### **Request Body:**
```json
{
"action": "approve|revision|reject",
"message": "Your message here"
}
```
### **Examples:**
#### **Approve:**
```bash
curl -X POST "http://localhost:8080/api/article-approval-flows/articles/123/approve" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer YOUR_TOKEN" \
-d '{
"action": "approve",
"message": "Content looks good, approved"
}'
```
#### **Revision:**
```bash
curl -X POST "http://localhost:8080/api/article-approval-flows/articles/123/approve" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer YOUR_TOKEN" \
-d '{
"action": "revision",
"message": "Please fix grammar and improve conclusion"
}'
```
#### **Reject:**
```bash
curl -X POST "http://localhost:8080/api/article-approval-flows/articles/123/approve" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer YOUR_TOKEN" \
-d '{
"action": "reject",
"message": "Content does not meet quality standards"
}'
```
## 📈 **Benefits**
### **1. Consistency**
- ✅ Semua action menggunakan field yang sama untuk message
- ✅ Tidak ada duplikasi field `RejectionReason` vs `RevisionMessage`
- ✅ Konsisten dengan naming convention
### **2. Simplicity**
- ✅ Satu endpoint untuk semua action types
- ✅ Satu field untuk semua message types
- ✅ StatusId sebagai pembeda utama
### **3. Maintainability**
- ✅ Code lebih mudah di-maintain
- ✅ Tidak ada duplikasi logic
- ✅ Single source of truth untuk message
### **4. Backward Compatibility**
- ✅ Field `revision_message` tetap ada dan digunakan
- ✅ Field `rejection_reason` tetap ada tapi tidak digunakan
- ✅ Tidak ada breaking change untuk database schema
## 🔄 **Migration Guide**
### **Untuk Client yang Menggunakan API Lama:**
#### **Before (Multiple Endpoints):**
```bash
# Reject
PUT /article-approval-flows/{id}/reject
{"reason": "Content not suitable"}
# Request Revision
PUT /article-approval-flows/{id}/request-revision
{"message": "Please fix grammar"}
```
#### **After (Single Endpoint):**
```bash
# Reject
POST /article-approval-flows/articles/{articleId}/approve
{"action": "reject", "message": "Content not suitable"}
# Request Revision
POST /article-approval-flows/articles/{articleId}/approve
{"action": "revision", "message": "Please fix grammar"}
```
## 🎉 **Summary**
Refactoring ini berhasil:
1. **✅ Menghilangkan duplikasi** field message
2. **✅ Menyederhanakan** API dengan single endpoint
3. **✅ Menyelaraskan** implementasi dengan field yang sama
4. **✅ Mempertahankan** backward compatibility
5. **✅ Meningkatkan** maintainability code
Sekarang sistem approval menggunakan implementasi yang lebih konsisten dan mudah di-maintain! 🚀

View File

@ -0,0 +1,196 @@
# Updated Approval Flow Logic
## 🎯 **Overview**
Flow approval telah diubah sesuai permintaan untuk memberikan pengalaman yang lebih baik dalam proses revisi dan penolakan artikel.
## 🔄 **Flow Behavior Changes**
### **1. Approve Action**
- ✅ **Behavior**: Tetap sama seperti sebelumnya
- ✅ **Result**: Naik ke step berikutnya atau selesai jika sudah step terakhir
- ✅ **Status**: Workflow berlanjut normal
### **2. Revision Action (Baru)**
- ✅ **Behavior**: Mundur ke step awal (step 1) dengan status pending
- ✅ **Result**: Flow approval direset seperti belum berjalan
- ✅ **Status**: `StatusId = 1` (pending)
- ✅ **Article Status**: `StatusId = 1` (draft), `IsDraft = true`
- ✅ **Restart**: Ketika user melakukan update artikel, approval akan berjalan kembali dari step 1
### **3. Reject Action (Baru)**
- ✅ **Behavior**: Mundur ke step awal (step 1) dengan status pending
- ✅ **Result**: Flow approval direset seperti belum berjalan
- ✅ **Status**: `StatusId = 1` (pending)
- ✅ **Article Status**: `StatusId = 1` (draft), `IsDraft = true`
- ✅ **Restart**: Ketika user melakukan update artikel, approval akan berjalan kembali dari step 1
## 📊 **Database Changes**
### **Article Approval Flows Table:**
```sql
-- Untuk Revision Action
UPDATE article_approval_flows
SET current_step = 1,
status_id = 1, -- pending
revision_requested = true,
revision_message = 'message',
completed_at = NULL -- Reset completed_at
WHERE id = flow_id;
-- Untuk Reject Action
UPDATE article_approval_flows
SET current_step = 1,
status_id = 1, -- pending
revision_message = 'message',
completed_at = NULL -- Reset completed_at
WHERE id = flow_id;
```
### **Articles Table:**
```sql
-- Untuk Revision & Reject Action
UPDATE articles
SET status_id = 1, -- draft
is_draft = true,
workflow_id = workflow_id, -- Keep workflow ID
current_approval_step = 1 -- Reset to step 1
WHERE id = article_id;
```
## 🎯 **Action Types & Behavior Summary**
| Action | Flow Behavior | Article Status | Restart Behavior |
|--------|---------------|----------------|------------------|
| **`approve`** | Naik ke step berikutnya | Tetap dalam approval | Workflow berlanjut |
| **`revision`** | Reset ke step 1, status pending | Draft | Restart dari step 1 saat update |
| **`reject`** | Reset ke step 1, status pending | Draft | Restart dari step 1 saat update |
## 🚀 **API Usage Examples**
### **Revision Request:**
```bash
curl -X POST "http://localhost:8080/api/article-approval-flows/articles/123/approve" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer YOUR_TOKEN" \
-d '{
"action": "revision",
"message": "Please fix grammar and improve conclusion"
}'
```
**Response:**
```json
{
"success": true,
"messages": ["Article successfully revision through active approval flow"],
"data": {
"article_id": 123,
"flow_id": 456,
"action": "revision",
"current_step": 1,
"current_branch": "Branch_A",
"workflow_id": 1
}
}
```
### **Reject Request:**
```bash
curl -X POST "http://localhost:8080/api/article-approval-flows/articles/123/approve" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer YOUR_TOKEN" \
-d '{
"action": "reject",
"message": "Content does not meet quality standards"
}'
```
**Response:**
```json
{
"success": true,
"messages": ["Article successfully reject through active approval flow"],
"data": {
"article_id": 123,
"flow_id": 456,
"action": "reject",
"current_step": 1,
"current_branch": "Branch_A",
"workflow_id": 1
}
}
```
## 🔄 **Restart Flow Logic**
### **Ketika User Update Artikel:**
1. **System Check**: Apakah artikel memiliki `workflow_id` dan `current_approval_step = 1`?
2. **Auto Restart**: Jika ya, approval flow akan berjalan kembali dari step 1
3. **Status Update**: Artikel status berubah dari draft ke pending approval
4. **Flow Continuation**: Approval flow berlanjut dengan step yang sudah direset
### **Implementation dalam Articles Service:**
```go
// Ketika artikel diupdate, check apakah perlu restart approval
if article.WorkflowId != nil && article.CurrentApprovalStep != nil && *article.CurrentApprovalStep == 1 {
// Restart approval flow
err := articleApprovalFlowsService.RestartApprovalFlow(article.ID)
if err != nil {
return err
}
}
```
## 📈 **Benefits**
### **1. Better User Experience**
- ✅ User bisa melakukan revisi tanpa kehilangan progress workflow
- ✅ Reject tidak menghapus workflow, hanya reset
- ✅ Mudah untuk restart approval setelah update
### **2. Workflow Continuity**
- ✅ Workflow ID tetap tersimpan untuk restart
- ✅ Tidak perlu membuat workflow baru
- ✅ History approval tetap tersimpan
### **3. Flexible Process**
- ✅ User bisa update artikel dan approval otomatis restart
- ✅ Tidak ada duplikasi workflow
- ✅ Konsisten dengan business logic
## 🎯 **Use Cases**
### **Scenario 1: Revision Process**
1. Editor review artikel di step 2
2. Editor request revision → Flow reset ke step 1, artikel jadi draft
3. Author update artikel
4. Approval flow restart otomatis dari step 1
5. Editor review lagi dari awal
### **Scenario 2: Rejection Process**
1. Editor review artikel di step 2
2. Editor reject artikel → Flow reset ke step 1, artikel jadi draft
3. Author update artikel
4. Approval flow restart otomatis dari step 1
5. Editor review lagi dari awal
### **Scenario 3: Multiple Revisions**
1. Editor request revision → Reset ke step 1
2. Author update → Restart approval
3. Editor request revision lagi → Reset ke step 1 lagi
4. Author update lagi → Restart approval lagi
5. Proses bisa berulang sampai approved
## 🎉 **Summary**
Perubahan flow ini memberikan:
1. **✅ Flexibility**: User bisa melakukan revisi/reject tanpa kehilangan workflow
2. **✅ Continuity**: Workflow tidak terputus, hanya direset
3. **✅ Auto Restart**: Approval otomatis restart saat artikel diupdate
4. **✅ Better UX**: Proses yang lebih intuitif dan user-friendly
5. **✅ Data Integrity**: Workflow ID dan history tetap tersimpan
Sekarang sistem approval memberikan pengalaman yang lebih baik dengan flow yang lebih fleksibel! 🚀

View File

@ -3267,150 +3267,6 @@ const docTemplate = `{
}
}
},
"/article-approval-flows/{id}/reject": {
"put": {
"security": [
{
"Bearer": []
}
],
"description": "API for rejecting article",
"tags": [
"ArticleApprovalFlows"
],
"summary": "Reject article",
"parameters": [
{
"type": "string",
"description": "Insert the Authorization",
"name": "Authorization",
"in": "header",
"required": true
},
{
"type": "string",
"default": "Bearer \u003cAdd access token here\u003e",
"description": "Insert your access token",
"name": "Authorization",
"in": "header"
},
{
"type": "integer",
"description": "ArticleApprovalFlows ID",
"name": "id",
"in": "path",
"required": true
},
{
"description": "Rejection data",
"name": "req",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/request.RejectionRequest"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/response.Response"
}
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/response.BadRequestError"
}
},
"401": {
"description": "Unauthorized",
"schema": {
"$ref": "#/definitions/response.UnauthorizedError"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/response.InternalServerError"
}
}
}
}
},
"/article-approval-flows/{id}/request-revision": {
"put": {
"security": [
{
"Bearer": []
}
],
"description": "API for requesting revision for article",
"tags": [
"ArticleApprovalFlows"
],
"summary": "Request revision for article",
"parameters": [
{
"type": "string",
"description": "Insert the Authorization",
"name": "Authorization",
"in": "header",
"required": true
},
{
"type": "string",
"default": "Bearer \u003cAdd access token here\u003e",
"description": "Insert your access token",
"name": "Authorization",
"in": "header"
},
{
"type": "integer",
"description": "ArticleApprovalFlows ID",
"name": "id",
"in": "path",
"required": true
},
{
"description": "Revision request data",
"name": "req",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/request.RevisionRequest"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/response.Response"
}
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/response.BadRequestError"
}
},
"401": {
"description": "Unauthorized",
"schema": {
"$ref": "#/definitions/response.UnauthorizedError"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/response.InternalServerError"
}
}
}
}
},
"/article-approval-flows/{id}/resubmit": {
"put": {
"security": [
@ -7827,6 +7683,15 @@ const docTemplate = `{
"name": "Authorization",
"in": "header"
},
{
"description": "Required payload",
"name": "payload",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/request.ArticlesUpdateRequest"
}
},
{
"type": "integer",
"description": "Articles ID",
@ -16475,7 +16340,18 @@ const docTemplate = `{
},
"request.ApprovalActionRequest": {
"type": "object",
"required": [
"action"
],
"properties": {
"action": {
"type": "string",
"enum": [
"approve",
"revision",
"reject"
]
},
"message": {
"type": "string"
}
@ -17086,6 +16962,59 @@ const docTemplate = `{
}
}
},
"request.ArticlesUpdateRequest": {
"type": "object",
"required": [
"categoryIds",
"description",
"htmlDescription",
"slug",
"tags",
"title",
"typeId"
],
"properties": {
"aiArticleId": {
"type": "integer"
},
"categoryIds": {
"type": "string"
},
"createdAt": {
"type": "string"
},
"createdById": {
"type": "integer"
},
"description": {
"type": "string"
},
"htmlDescription": {
"type": "string"
},
"isDraft": {
"type": "boolean"
},
"isPublish": {
"type": "boolean"
},
"slug": {
"type": "string"
},
"statusId": {
"type": "integer"
},
"tags": {
"type": "string"
},
"title": {
"type": "string"
},
"typeId": {
"type": "integer"
}
}
},
"request.BookmarksCreateRequest": {
"type": "object",
"required": [
@ -17744,17 +17673,6 @@ const docTemplate = `{
}
}
},
"request.RejectionRequest": {
"type": "object",
"required": [
"reason"
],
"properties": {
"reason": {
"type": "string"
}
}
},
"request.ReorderApprovalWorkflowStepsRequest": {
"type": "object",
"required": [
@ -17791,17 +17709,6 @@ const docTemplate = `{
}
}
},
"request.RevisionRequest": {
"type": "object",
"required": [
"message"
],
"properties": {
"message": {
"type": "string"
}
}
},
"request.SchedulesCreateRequest": {
"type": "object",
"required": [

View File

@ -3256,150 +3256,6 @@
}
}
},
"/article-approval-flows/{id}/reject": {
"put": {
"security": [
{
"Bearer": []
}
],
"description": "API for rejecting article",
"tags": [
"ArticleApprovalFlows"
],
"summary": "Reject article",
"parameters": [
{
"type": "string",
"description": "Insert the Authorization",
"name": "Authorization",
"in": "header",
"required": true
},
{
"type": "string",
"default": "Bearer \u003cAdd access token here\u003e",
"description": "Insert your access token",
"name": "Authorization",
"in": "header"
},
{
"type": "integer",
"description": "ArticleApprovalFlows ID",
"name": "id",
"in": "path",
"required": true
},
{
"description": "Rejection data",
"name": "req",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/request.RejectionRequest"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/response.Response"
}
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/response.BadRequestError"
}
},
"401": {
"description": "Unauthorized",
"schema": {
"$ref": "#/definitions/response.UnauthorizedError"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/response.InternalServerError"
}
}
}
}
},
"/article-approval-flows/{id}/request-revision": {
"put": {
"security": [
{
"Bearer": []
}
],
"description": "API for requesting revision for article",
"tags": [
"ArticleApprovalFlows"
],
"summary": "Request revision for article",
"parameters": [
{
"type": "string",
"description": "Insert the Authorization",
"name": "Authorization",
"in": "header",
"required": true
},
{
"type": "string",
"default": "Bearer \u003cAdd access token here\u003e",
"description": "Insert your access token",
"name": "Authorization",
"in": "header"
},
{
"type": "integer",
"description": "ArticleApprovalFlows ID",
"name": "id",
"in": "path",
"required": true
},
{
"description": "Revision request data",
"name": "req",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/request.RevisionRequest"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/response.Response"
}
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/response.BadRequestError"
}
},
"401": {
"description": "Unauthorized",
"schema": {
"$ref": "#/definitions/response.UnauthorizedError"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/response.InternalServerError"
}
}
}
}
},
"/article-approval-flows/{id}/resubmit": {
"put": {
"security": [
@ -7816,6 +7672,15 @@
"name": "Authorization",
"in": "header"
},
{
"description": "Required payload",
"name": "payload",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/request.ArticlesUpdateRequest"
}
},
{
"type": "integer",
"description": "Articles ID",
@ -16464,7 +16329,18 @@
},
"request.ApprovalActionRequest": {
"type": "object",
"required": [
"action"
],
"properties": {
"action": {
"type": "string",
"enum": [
"approve",
"revision",
"reject"
]
},
"message": {
"type": "string"
}
@ -17075,6 +16951,59 @@
}
}
},
"request.ArticlesUpdateRequest": {
"type": "object",
"required": [
"categoryIds",
"description",
"htmlDescription",
"slug",
"tags",
"title",
"typeId"
],
"properties": {
"aiArticleId": {
"type": "integer"
},
"categoryIds": {
"type": "string"
},
"createdAt": {
"type": "string"
},
"createdById": {
"type": "integer"
},
"description": {
"type": "string"
},
"htmlDescription": {
"type": "string"
},
"isDraft": {
"type": "boolean"
},
"isPublish": {
"type": "boolean"
},
"slug": {
"type": "string"
},
"statusId": {
"type": "integer"
},
"tags": {
"type": "string"
},
"title": {
"type": "string"
},
"typeId": {
"type": "integer"
}
}
},
"request.BookmarksCreateRequest": {
"type": "object",
"required": [
@ -17733,17 +17662,6 @@
}
}
},
"request.RejectionRequest": {
"type": "object",
"required": [
"reason"
],
"properties": {
"reason": {
"type": "string"
}
}
},
"request.ReorderApprovalWorkflowStepsRequest": {
"type": "object",
"required": [
@ -17780,17 +17698,6 @@
}
}
},
"request.RevisionRequest": {
"type": "object",
"required": [
"message"
],
"properties": {
"message": {
"type": "string"
}
}
},
"request.SchedulesCreateRequest": {
"type": "object",
"required": [

View File

@ -142,8 +142,16 @@ definitions:
type: object
request.ApprovalActionRequest:
properties:
action:
enum:
- approve
- revision
- reject
type: string
message:
type: string
required:
- action
type: object
request.ApprovalWorkflowStepRequest:
properties:
@ -561,6 +569,43 @@ definitions:
- title
- typeId
type: object
request.ArticlesUpdateRequest:
properties:
aiArticleId:
type: integer
categoryIds:
type: string
createdAt:
type: string
createdById:
type: integer
description:
type: string
htmlDescription:
type: string
isDraft:
type: boolean
isPublish:
type: boolean
slug:
type: string
statusId:
type: integer
tags:
type: string
title:
type: string
typeId:
type: integer
required:
- categoryIds
- description
- htmlDescription
- slug
- tags
- title
- typeId
type: object
request.BookmarksCreateRequest:
properties:
articleId:
@ -1014,13 +1059,6 @@ definitions:
- statusId
- userId
type: object
request.RejectionRequest:
properties:
reason:
type: string
required:
- reason
type: object
request.ReorderApprovalWorkflowStepsRequest:
properties:
stepOrders:
@ -1045,13 +1083,6 @@ definitions:
message:
type: string
type: object
request.RevisionRequest:
properties:
message:
type: string
required:
- message
type: object
request.SchedulesCreateRequest:
properties:
createdById:
@ -3245,100 +3276,6 @@ paths:
summary: Get next steps preview for multi-branch workflow
tags:
- ArticleApprovalFlows
/article-approval-flows/{id}/reject:
put:
description: API for rejecting article
parameters:
- description: Insert the Authorization
in: header
name: Authorization
required: true
type: string
- default: Bearer <Add access token here>
description: Insert your access token
in: header
name: Authorization
type: string
- description: ArticleApprovalFlows ID
in: path
name: id
required: true
type: integer
- description: Rejection data
in: body
name: req
required: true
schema:
$ref: '#/definitions/request.RejectionRequest'
responses:
"200":
description: OK
schema:
$ref: '#/definitions/response.Response'
"400":
description: Bad Request
schema:
$ref: '#/definitions/response.BadRequestError'
"401":
description: Unauthorized
schema:
$ref: '#/definitions/response.UnauthorizedError'
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/response.InternalServerError'
security:
- Bearer: []
summary: Reject article
tags:
- ArticleApprovalFlows
/article-approval-flows/{id}/request-revision:
put:
description: API for requesting revision for article
parameters:
- description: Insert the Authorization
in: header
name: Authorization
required: true
type: string
- default: Bearer <Add access token here>
description: Insert your access token
in: header
name: Authorization
type: string
- description: ArticleApprovalFlows ID
in: path
name: id
required: true
type: integer
- description: Revision request data
in: body
name: req
required: true
schema:
$ref: '#/definitions/request.RevisionRequest'
responses:
"200":
description: OK
schema:
$ref: '#/definitions/response.Response'
"400":
description: Bad Request
schema:
$ref: '#/definitions/response.BadRequestError'
"401":
description: Unauthorized
schema:
$ref: '#/definitions/response.UnauthorizedError'
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/response.InternalServerError'
security:
- Bearer: []
summary: Request revision for article
tags:
- ArticleApprovalFlows
/article-approval-flows/{id}/resubmit:
put:
description: API for resubmitting article after revision
@ -6193,6 +6130,12 @@ paths:
in: header
name: Authorization
type: string
- description: Required payload
in: body
name: payload
required: true
schema:
$ref: '#/definitions/request.ArticlesUpdateRequest'
- description: Articles ID
in: path
name: id