Add Surat Keluar
This commit is contained in:
parent
0399c87736
commit
e4946d6e05
@ -197,6 +197,14 @@ type processors struct {
|
|||||||
recipientProcessor *processor.RecipientProcessorImpl
|
recipientProcessor *processor.RecipientProcessorImpl
|
||||||
letterDispositionProcessor *processor.LetterDispositionProcessorImpl
|
letterDispositionProcessor *processor.LetterDispositionProcessorImpl
|
||||||
letterDispositionDeptProcessor *processor.LetterDispositionDepartmentProcessorImpl
|
letterDispositionDeptProcessor *processor.LetterDispositionDepartmentProcessorImpl
|
||||||
|
// Modular processors for letter outgoing
|
||||||
|
letterValidationProcessor processor.LetterValidationProcessor
|
||||||
|
letterCreationProcessor processor.LetterCreationProcessor
|
||||||
|
letterApprovalProcessor processor.LetterApprovalProcessor
|
||||||
|
letterAttachmentProcessor processor.LetterAttachmentProcessor
|
||||||
|
letterOutgoingRecipientProcessor processor.LetterOutgoingRecipientProcessor
|
||||||
|
letterActivityProcessor processor.LetterActivityProcessor
|
||||||
|
txManager *repository.TxManager
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *App) initProcessors(cfg *config.Config, repos *repositories) *processors {
|
func (a *App) initProcessors(cfg *config.Config, repos *repositories) *processors {
|
||||||
@ -218,6 +226,31 @@ func (a *App) initProcessors(cfg *config.Config, repos *repositories) *processor
|
|||||||
letterNumberGen, repos.dispositionRouteRepo,
|
letterNumberGen, repos.dispositionRouteRepo,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Create modular processors for letter outgoing
|
||||||
|
letterValidationProc := processor.NewLetterValidationProcessor()
|
||||||
|
letterCreationProc := processor.NewLetterCreationProcessor(
|
||||||
|
repos.letterOutgoingRepo,
|
||||||
|
repos.approvalFlowRepo,
|
||||||
|
letterNumberGen,
|
||||||
|
)
|
||||||
|
letterApprovalProc := processor.NewLetterApprovalProcessor(
|
||||||
|
repos.letterOutgoingApprovalRepo,
|
||||||
|
repos.approvalFlowRepo,
|
||||||
|
repos.letterOutgoingRepo,
|
||||||
|
)
|
||||||
|
letterAttachmentProc := processor.NewLetterAttachmentProcessor(
|
||||||
|
repos.letterOutgoingAttachmentRepo,
|
||||||
|
)
|
||||||
|
letterOutgoingRecipientProc := processor.NewLetterOutgoingRecipientProcessor(
|
||||||
|
repos.letterOutgoingRecipientRepo,
|
||||||
|
repos.approvalFlowRepo,
|
||||||
|
repos.userDeptRepo,
|
||||||
|
)
|
||||||
|
letterActivityProc := processor.NewLetterActivityProcessor(
|
||||||
|
repos.letterOutgoingActivityLogRepo,
|
||||||
|
)
|
||||||
|
|
||||||
|
// Create the main letter outgoing processor for backward compatibility
|
||||||
letterOutgoingProc := processor.NewLetterOutgoingProcessor(
|
letterOutgoingProc := processor.NewLetterOutgoingProcessor(
|
||||||
a.db,
|
a.db,
|
||||||
repos.letterOutgoingRepo,
|
repos.letterOutgoingRepo,
|
||||||
@ -307,6 +340,14 @@ func (a *App) initProcessors(cfg *config.Config, repos *repositories) *processor
|
|||||||
recipientProcessor: recipientProc,
|
recipientProcessor: recipientProc,
|
||||||
letterDispositionProcessor: letterDispositionProc,
|
letterDispositionProcessor: letterDispositionProc,
|
||||||
letterDispositionDeptProcessor: letterDispositionDeptProc,
|
letterDispositionDeptProcessor: letterDispositionDeptProc,
|
||||||
|
// Modular processors
|
||||||
|
letterValidationProcessor: letterValidationProc,
|
||||||
|
letterCreationProcessor: letterCreationProc,
|
||||||
|
letterApprovalProcessor: letterApprovalProc,
|
||||||
|
letterAttachmentProcessor: letterAttachmentProc,
|
||||||
|
letterOutgoingRecipientProcessor: letterOutgoingRecipientProc,
|
||||||
|
letterActivityProcessor: letterActivityProc,
|
||||||
|
txManager: txMgr,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -352,7 +393,16 @@ func (a *App) initServices(processors *processors, repos *repositories, cfg *con
|
|||||||
processors.activityLogger,
|
processors.activityLogger,
|
||||||
)
|
)
|
||||||
dispRouteSvc := service.NewDispositionRouteService(repos.dispositionRouteRepo)
|
dispRouteSvc := service.NewDispositionRouteService(repos.dispositionRouteRepo)
|
||||||
letterOutgoingSvc := service.NewLetterOutgoingService(processors.letterOutgoingProcessor)
|
letterOutgoingSvc := service.NewLetterOutgoingService(
|
||||||
|
processors.letterOutgoingProcessor,
|
||||||
|
processors.txManager,
|
||||||
|
processors.letterValidationProcessor,
|
||||||
|
processors.letterCreationProcessor,
|
||||||
|
processors.letterApprovalProcessor,
|
||||||
|
processors.letterAttachmentProcessor,
|
||||||
|
processors.letterOutgoingRecipientProcessor,
|
||||||
|
processors.letterActivityProcessor,
|
||||||
|
)
|
||||||
|
|
||||||
approvalFlowStepRepo := repository.NewApprovalFlowStepRepository(a.db)
|
approvalFlowStepRepo := repository.NewApprovalFlowStepRepository(a.db)
|
||||||
approvalFlowSvc := service.NewApprovalFlowService(
|
approvalFlowSvc := service.NewApprovalFlowService(
|
||||||
|
|||||||
@ -7,12 +7,17 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type Department struct {
|
type Department struct {
|
||||||
ID uuid.UUID `gorm:"type:uuid;primary_key;default:gen_random_uuid()" json:"id"`
|
ID uuid.UUID `gorm:"type:uuid;primary_key;default:gen_random_uuid()" json:"id"`
|
||||||
Name string `gorm:"not null" json:"name"`
|
Name string `gorm:"not null" json:"name"`
|
||||||
Code string `json:"code,omitempty"`
|
Code string `json:"code,omitempty"`
|
||||||
Path string `gorm:"not null" json:"path"`
|
Path string `gorm:"not null" json:"path"`
|
||||||
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
|
ParentDepartmentID *uuid.UUID `gorm:"type:uuid" json:"parent_department_id,omitempty"`
|
||||||
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"`
|
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
|
||||||
|
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"`
|
||||||
|
|
||||||
|
// Relations
|
||||||
|
ParentDepartment *Department `gorm:"foreignKey:ParentDepartmentID" json:"parent_department,omitempty"`
|
||||||
|
ChildDepartments []Department `gorm:"foreignKey:ParentDepartmentID" json:"child_departments,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (Department) TableName() string { return "departments" }
|
func (Department) TableName() string { return "departments" }
|
||||||
|
|||||||
270
internal/processor/department_processor.go
Normal file
270
internal/processor/department_processor.go
Normal file
@ -0,0 +1,270 @@
|
|||||||
|
package processor
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"eslogad-be/internal/entities"
|
||||||
|
"eslogad-be/internal/repository"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
// DepartmentProcessor handles all department-related business logic
|
||||||
|
type DepartmentProcessor interface {
|
||||||
|
Create(ctx context.Context, department *entities.Department) error
|
||||||
|
Get(ctx context.Context, id uuid.UUID) (*entities.Department, error)
|
||||||
|
GetByPath(ctx context.Context, path string) (*entities.Department, error)
|
||||||
|
Update(ctx context.Context, department *entities.Department) error
|
||||||
|
Delete(ctx context.Context, id uuid.UUID) error
|
||||||
|
List(ctx context.Context, search string, limit, offset int) ([]entities.Department, int64, error)
|
||||||
|
|
||||||
|
// Hierarchy operations
|
||||||
|
GetChildren(ctx context.Context, parentID uuid.UUID) ([]entities.Department, error)
|
||||||
|
GetAllDescendants(ctx context.Context, parentID uuid.UUID) ([]entities.Department, error)
|
||||||
|
UpdateChildrenPaths(ctx context.Context, oldPath, newPath string) error
|
||||||
|
ValidateHierarchy(ctx context.Context, departmentID, newParentID uuid.UUID) error
|
||||||
|
|
||||||
|
// Tree building operations
|
||||||
|
GetDepartmentWithDescendants(ctx context.Context, departmentID uuid.UUID) ([]entities.Department, error)
|
||||||
|
GetDepartmentWithParentAndSiblings(ctx context.Context, departmentID uuid.UUID) ([]entities.Department, error)
|
||||||
|
GetAllDepartments(ctx context.Context) ([]entities.Department, error)
|
||||||
|
GetDepartmentsByRootPath(ctx context.Context, rootPath string) ([]entities.Department, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type DepartmentProcessorImpl struct {
|
||||||
|
departmentRepo *repository.DepartmentRepository
|
||||||
|
txManager *repository.TxManager
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewDepartmentProcessor creates a new department processor
|
||||||
|
func NewDepartmentProcessor(
|
||||||
|
departmentRepo *repository.DepartmentRepository,
|
||||||
|
txManager *repository.TxManager,
|
||||||
|
) *DepartmentProcessorImpl {
|
||||||
|
return &DepartmentProcessorImpl{
|
||||||
|
departmentRepo: departmentRepo,
|
||||||
|
txManager: txManager,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create creates a new department
|
||||||
|
func (p *DepartmentProcessorImpl) Create(ctx context.Context, department *entities.Department) error {
|
||||||
|
// Build path based on parent
|
||||||
|
if department.ParentDepartmentID != nil && *department.ParentDepartmentID != uuid.Nil {
|
||||||
|
parent, err := p.departmentRepo.GetByID(ctx, *department.ParentDepartmentID)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("parent department not found: %w", err)
|
||||||
|
}
|
||||||
|
department.Path = parent.Path + "." + department.Code
|
||||||
|
} else {
|
||||||
|
department.Path = department.Code
|
||||||
|
}
|
||||||
|
|
||||||
|
return p.departmentRepo.Create(ctx, department)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get retrieves a department by ID
|
||||||
|
func (p *DepartmentProcessorImpl) Get(ctx context.Context, id uuid.UUID) (*entities.Department, error) {
|
||||||
|
return p.departmentRepo.Get(ctx, id)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetByPath retrieves a department by its path
|
||||||
|
func (p *DepartmentProcessorImpl) GetByPath(ctx context.Context, path string) (*entities.Department, error) {
|
||||||
|
return p.departmentRepo.GetByPath(ctx, path)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update updates a department with hierarchy validation
|
||||||
|
func (p *DepartmentProcessorImpl) Update(ctx context.Context, department *entities.Department) error {
|
||||||
|
return p.txManager.WithTransaction(ctx, func(txCtx context.Context) error {
|
||||||
|
// Get the current department state
|
||||||
|
current, err := p.departmentRepo.Get(txCtx, department.ID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store old values for comparison
|
||||||
|
oldPath := current.Path
|
||||||
|
oldParentID := current.ParentDepartmentID
|
||||||
|
pathChanged := false
|
||||||
|
|
||||||
|
// Update path if parent or code changed
|
||||||
|
if department.ParentDepartmentID != oldParentID || department.Code != current.Code {
|
||||||
|
// Rebuild path
|
||||||
|
if department.ParentDepartmentID == nil {
|
||||||
|
department.Path = department.Code
|
||||||
|
} else {
|
||||||
|
parent, err := p.departmentRepo.GetByID(txCtx, *department.ParentDepartmentID)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("parent department not found: %w", err)
|
||||||
|
}
|
||||||
|
department.Path = parent.Path + "." + department.Code
|
||||||
|
}
|
||||||
|
|
||||||
|
if department.Path != oldPath {
|
||||||
|
pathChanged = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the department
|
||||||
|
if err := p.departmentRepo.Update(txCtx, department); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update children paths if necessary
|
||||||
|
if pathChanged {
|
||||||
|
if err := p.updateChildrenPathsRecursively(txCtx, department.ID, department.Path); err != nil {
|
||||||
|
return fmt.Errorf("failed to update children paths: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete deletes a department
|
||||||
|
func (p *DepartmentProcessorImpl) Delete(ctx context.Context, id uuid.UUID) error {
|
||||||
|
return p.departmentRepo.Delete(ctx, id)
|
||||||
|
}
|
||||||
|
|
||||||
|
// List lists departments with pagination
|
||||||
|
func (p *DepartmentProcessorImpl) List(ctx context.Context, search string, limit, offset int) ([]entities.Department, int64, error) {
|
||||||
|
return p.departmentRepo.List(ctx, search, limit, offset)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetChildren gets direct children of a department
|
||||||
|
func (p *DepartmentProcessorImpl) GetChildren(ctx context.Context, parentID uuid.UUID) ([]entities.Department, error) {
|
||||||
|
// First get the parent department to get its path
|
||||||
|
parent, err := p.departmentRepo.Get(ctx, parentID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if parent == nil {
|
||||||
|
return []entities.Department{}, nil
|
||||||
|
}
|
||||||
|
return p.departmentRepo.GetChildren(ctx, parent.Path)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetAllDescendants gets all descendants of a department
|
||||||
|
func (p *DepartmentProcessorImpl) GetAllDescendants(ctx context.Context, parentID uuid.UUID) ([]entities.Department, error) {
|
||||||
|
return p.departmentRepo.GetAllDescendants(ctx, parentID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateChildrenPaths updates paths for all children
|
||||||
|
func (p *DepartmentProcessorImpl) UpdateChildrenPaths(ctx context.Context, oldPath, newPath string) error {
|
||||||
|
return p.departmentRepo.UpdateChildrenPaths(ctx, oldPath, newPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ValidateHierarchy validates that setting newParentID won't create circular references
|
||||||
|
func (p *DepartmentProcessorImpl) ValidateHierarchy(ctx context.Context, departmentID, newParentID uuid.UUID) error {
|
||||||
|
// Can't be its own parent
|
||||||
|
if departmentID == newParentID {
|
||||||
|
return fmt.Errorf("department cannot be its own parent")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if new parent is a descendant
|
||||||
|
descendants, err := p.GetAllDescendants(ctx, departmentID)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to check descendants: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, desc := range descendants {
|
||||||
|
if desc.ID == newParentID {
|
||||||
|
return fmt.Errorf("cannot set a descendant as parent (circular reference)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetDepartmentWithDescendants gets a department and all its descendants
|
||||||
|
func (p *DepartmentProcessorImpl) GetDepartmentWithDescendants(ctx context.Context, departmentID uuid.UUID) ([]entities.Department, error) {
|
||||||
|
// Get the department
|
||||||
|
dept, err := p.departmentRepo.Get(ctx, departmentID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
departments := []entities.Department{*dept}
|
||||||
|
|
||||||
|
// Get all descendants
|
||||||
|
descendants, err := p.departmentRepo.GetAllDescendants(ctx, departmentID)
|
||||||
|
if err == nil {
|
||||||
|
departments = append(departments, descendants...)
|
||||||
|
}
|
||||||
|
|
||||||
|
return departments, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetDepartmentWithParentAndSiblings gets a department with its parent and all siblings
|
||||||
|
func (p *DepartmentProcessorImpl) GetDepartmentWithParentAndSiblings(ctx context.Context, departmentID uuid.UUID) ([]entities.Department, error) {
|
||||||
|
// Get the department
|
||||||
|
dept, err := p.departmentRepo.Get(ctx, departmentID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var departments []entities.Department
|
||||||
|
|
||||||
|
// If has parent, get parent and all its descendants
|
||||||
|
if dept.ParentDepartmentID != nil {
|
||||||
|
parent, err := p.departmentRepo.Get(ctx, *dept.ParentDepartmentID)
|
||||||
|
if err == nil {
|
||||||
|
departments = append(departments, *parent)
|
||||||
|
|
||||||
|
// Get all descendants of parent (includes siblings)
|
||||||
|
descendants, err := p.departmentRepo.GetAllDescendants(ctx, parent.ID)
|
||||||
|
if err == nil {
|
||||||
|
departments = append(departments, descendants...)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Fallback to just department and its descendants
|
||||||
|
return p.GetDepartmentWithDescendants(ctx, departmentID)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// No parent, just get department and descendants
|
||||||
|
return p.GetDepartmentWithDescendants(ctx, departmentID)
|
||||||
|
}
|
||||||
|
|
||||||
|
return departments, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetAllDepartments gets all departments in the system
|
||||||
|
func (p *DepartmentProcessorImpl) GetAllDepartments(ctx context.Context) ([]entities.Department, error) {
|
||||||
|
departments, _, err := p.departmentRepo.List(ctx, "", 0, 0)
|
||||||
|
return departments, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetDepartmentsByRootPath gets departments starting from a specific path
|
||||||
|
func (p *DepartmentProcessorImpl) GetDepartmentsByRootPath(ctx context.Context, rootPath string) ([]entities.Department, error) {
|
||||||
|
// Find the root department
|
||||||
|
rootDept, err := p.departmentRepo.GetByPath(ctx, rootPath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return p.GetDepartmentWithDescendants(ctx, rootDept.ID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// updateChildrenPathsRecursively updates paths for all children recursively
|
||||||
|
func (p *DepartmentProcessorImpl) updateChildrenPathsRecursively(ctx context.Context, parentID uuid.UUID, parentPath string) error {
|
||||||
|
children, err := p.departmentRepo.GetChildren(ctx, parentPath)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, child := range children {
|
||||||
|
// Update child's path
|
||||||
|
child.Path = parentPath + "." + child.Code
|
||||||
|
if err := p.departmentRepo.Update(ctx, &child); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Recursively update grandchildren
|
||||||
|
if err := p.updateChildrenPathsRecursively(ctx, child.ID, child.Path); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
52
internal/processor/letter_activity_processor.go
Normal file
52
internal/processor/letter_activity_processor.go
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
package processor
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"eslogad-be/internal/entities"
|
||||||
|
"eslogad-be/internal/repository"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
type LetterActivityProcessor interface {
|
||||||
|
LogActivity(ctx context.Context, letterID uuid.UUID, action string, userID uuid.UUID, details map[string]interface{}) error
|
||||||
|
LogStatusChange(ctx context.Context, letterID uuid.UUID, fromStatus, toStatus string, userID uuid.UUID) error
|
||||||
|
GetActivitiesByLetterID(ctx context.Context, letterID uuid.UUID) ([]entities.LetterOutgoingActivityLog, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type LetterActivityProcessorImpl struct {
|
||||||
|
activityLogRepo *repository.LetterOutgoingActivityLogRepository
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewLetterActivityProcessor(activityLogRepo *repository.LetterOutgoingActivityLogRepository) *LetterActivityProcessorImpl {
|
||||||
|
return &LetterActivityProcessorImpl{
|
||||||
|
activityLogRepo: activityLogRepo,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *LetterActivityProcessorImpl) LogActivity(ctx context.Context, letterID uuid.UUID, action string, userID uuid.UUID, details map[string]interface{}) error {
|
||||||
|
log := &entities.LetterOutgoingActivityLog{
|
||||||
|
LetterID: letterID,
|
||||||
|
ActionType: action,
|
||||||
|
ActorUserID: &userID,
|
||||||
|
Context: details,
|
||||||
|
OccurredAt: time.Now(),
|
||||||
|
}
|
||||||
|
|
||||||
|
return p.activityLogRepo.Create(ctx, log)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *LetterActivityProcessorImpl) LogStatusChange(ctx context.Context, letterID uuid.UUID, fromStatus, toStatus string, userID uuid.UUID) error {
|
||||||
|
details := map[string]interface{}{
|
||||||
|
"from_status": fromStatus,
|
||||||
|
"to_status": toStatus,
|
||||||
|
}
|
||||||
|
|
||||||
|
return p.LogActivity(ctx, letterID, "status_changed", userID, details)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *LetterActivityProcessorImpl) GetActivitiesByLetterID(ctx context.Context, letterID uuid.UUID) ([]entities.LetterOutgoingActivityLog, error) {
|
||||||
|
return p.activityLogRepo.ListByLetter(ctx, letterID)
|
||||||
|
}
|
||||||
113
internal/processor/letter_approval_processor.go
Normal file
113
internal/processor/letter_approval_processor.go
Normal file
@ -0,0 +1,113 @@
|
|||||||
|
package processor
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"eslogad-be/internal/entities"
|
||||||
|
"eslogad-be/internal/repository"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
type LetterApprovalProcessor interface {
|
||||||
|
CreateApprovalSteps(ctx context.Context, letter *entities.LetterOutgoing) error
|
||||||
|
ProcessApprovalAction(ctx context.Context, letterID uuid.UUID, approval *entities.LetterOutgoingApproval, userID uuid.UUID, isApproval bool) error
|
||||||
|
UpdateApprovalStatus(ctx context.Context, approval *entities.LetterOutgoingApproval, status entities.ApprovalStatus, userID uuid.UUID) error
|
||||||
|
CheckAllApprovalsComplete(ctx context.Context, letterID uuid.UUID) (bool, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type LetterApprovalProcessorImpl struct {
|
||||||
|
approvalRepo *repository.LetterOutgoingApprovalRepository
|
||||||
|
approvalFlowRepo *repository.ApprovalFlowRepository
|
||||||
|
letterRepo *repository.LetterOutgoingRepository
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewLetterApprovalProcessor(
|
||||||
|
approvalRepo *repository.LetterOutgoingApprovalRepository,
|
||||||
|
approvalFlowRepo *repository.ApprovalFlowRepository,
|
||||||
|
letterRepo *repository.LetterOutgoingRepository,
|
||||||
|
) *LetterApprovalProcessorImpl {
|
||||||
|
return &LetterApprovalProcessorImpl{
|
||||||
|
approvalRepo: approvalRepo,
|
||||||
|
approvalFlowRepo: approvalFlowRepo,
|
||||||
|
letterRepo: letterRepo,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *LetterApprovalProcessorImpl) CreateApprovalSteps(ctx context.Context, letter *entities.LetterOutgoing) error {
|
||||||
|
if letter.ApprovalFlowID == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
flow, err := p.approvalFlowRepo.Get(ctx, *letter.ApprovalFlowID)
|
||||||
|
if err != nil || flow == nil || len(flow.Steps) == 0 {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find the minimum step order (first step)
|
||||||
|
minStepOrder := flow.Steps[0].StepOrder
|
||||||
|
for _, step := range flow.Steps {
|
||||||
|
if step.StepOrder < minStepOrder {
|
||||||
|
minStepOrder = step.StepOrder
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create approval records for each step
|
||||||
|
for _, step := range flow.Steps {
|
||||||
|
approval := entities.LetterOutgoingApproval{
|
||||||
|
LetterID: letter.ID,
|
||||||
|
StepID: step.ID,
|
||||||
|
StepOrder: step.StepOrder,
|
||||||
|
ParallelGroup: step.ParallelGroup,
|
||||||
|
IsRequired: step.Required,
|
||||||
|
ApproverID: step.ApproverUserID,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set initial status
|
||||||
|
if step.StepOrder == minStepOrder {
|
||||||
|
approval.Status = entities.ApprovalStatusPending
|
||||||
|
} else {
|
||||||
|
approval.Status = entities.ApprovalStatusNotStarted
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := p.approvalRepo.Create(ctx, &approval); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *LetterApprovalProcessorImpl) ProcessApprovalAction(ctx context.Context, letterID uuid.UUID, approval *entities.LetterOutgoingApproval, userID uuid.UUID, isApproval bool) error {
|
||||||
|
status := entities.ApprovalStatusApproved
|
||||||
|
if !isApproval {
|
||||||
|
status = entities.ApprovalStatusRejected
|
||||||
|
}
|
||||||
|
|
||||||
|
return p.UpdateApprovalStatus(ctx, approval, status, userID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *LetterApprovalProcessorImpl) UpdateApprovalStatus(ctx context.Context, approval *entities.LetterOutgoingApproval, status entities.ApprovalStatus, userID uuid.UUID) error {
|
||||||
|
approval.Status = status
|
||||||
|
approval.ApproverID = &userID
|
||||||
|
now := time.Now()
|
||||||
|
approval.ActedAt = &now
|
||||||
|
|
||||||
|
return p.approvalRepo.Update(ctx, approval)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *LetterApprovalProcessorImpl) CheckAllApprovalsComplete(ctx context.Context, letterID uuid.UUID) (bool, error) {
|
||||||
|
approvals, err := p.approvalRepo.ListByLetter(ctx, letterID)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, approval := range approvals {
|
||||||
|
if approval.IsRequired && approval.Status != entities.ApprovalStatusApproved {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
54
internal/processor/letter_attachment_processor.go
Normal file
54
internal/processor/letter_attachment_processor.go
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
package processor
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"eslogad-be/internal/entities"
|
||||||
|
"eslogad-be/internal/repository"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
type LetterAttachmentProcessor interface {
|
||||||
|
CreateAttachments(ctx context.Context, letterID uuid.UUID, attachments []entities.LetterOutgoingAttachment) error
|
||||||
|
RemoveAttachment(ctx context.Context, letterID uuid.UUID, attachmentID uuid.UUID) error
|
||||||
|
GetAttachmentsByLetterID(ctx context.Context, letterID uuid.UUID) ([]entities.LetterOutgoingAttachment, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type LetterAttachmentProcessorImpl struct {
|
||||||
|
attachmentRepo *repository.LetterOutgoingAttachmentRepository
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewLetterAttachmentProcessor(attachmentRepo *repository.LetterOutgoingAttachmentRepository) *LetterAttachmentProcessorImpl {
|
||||||
|
return &LetterAttachmentProcessorImpl{
|
||||||
|
attachmentRepo: attachmentRepo,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *LetterAttachmentProcessorImpl) CreateAttachments(ctx context.Context, letterID uuid.UUID, attachments []entities.LetterOutgoingAttachment) error {
|
||||||
|
if len(attachments) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set letter ID for all attachments
|
||||||
|
for i := range attachments {
|
||||||
|
attachments[i].LetterID = letterID
|
||||||
|
}
|
||||||
|
|
||||||
|
return p.attachmentRepo.CreateBulk(ctx, attachments)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *LetterAttachmentProcessorImpl) RemoveAttachment(ctx context.Context, letterID uuid.UUID, attachmentID uuid.UUID) error {
|
||||||
|
return p.attachmentRepo.Delete(ctx, attachmentID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *LetterAttachmentProcessorImpl) GetAttachmentsByLetterID(ctx context.Context, letterID uuid.UUID) ([]entities.LetterOutgoingAttachment, error) {
|
||||||
|
attachmentMap, err := p.attachmentRepo.ListByLetterIDs(ctx, []uuid.UUID{letterID})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if attachments, ok := attachmentMap[letterID]; ok {
|
||||||
|
return attachments, nil
|
||||||
|
}
|
||||||
|
return []entities.LetterOutgoingAttachment{}, nil
|
||||||
|
}
|
||||||
72
internal/processor/letter_creation_processor.go
Normal file
72
internal/processor/letter_creation_processor.go
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
package processor
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"eslogad-be/internal/entities"
|
||||||
|
"eslogad-be/internal/repository"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
type LetterCreationProcessor interface {
|
||||||
|
PrepareLetterForCreation(ctx context.Context, letter *entities.LetterOutgoing, departmentID uuid.UUID) error
|
||||||
|
CreateLetter(ctx context.Context, letter *entities.LetterOutgoing) error
|
||||||
|
GenerateLetterNumber(ctx context.Context, letter *entities.LetterOutgoing) error
|
||||||
|
}
|
||||||
|
|
||||||
|
type LetterCreationProcessorImpl struct {
|
||||||
|
letterRepo *repository.LetterOutgoingRepository
|
||||||
|
approvalFlowRepo *repository.ApprovalFlowRepository
|
||||||
|
numberGenerator LetterNumberGenerator
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewLetterCreationProcessor(
|
||||||
|
letterRepo *repository.LetterOutgoingRepository,
|
||||||
|
approvalFlowRepo *repository.ApprovalFlowRepository,
|
||||||
|
numberGenerator LetterNumberGenerator,
|
||||||
|
) *LetterCreationProcessorImpl {
|
||||||
|
return &LetterCreationProcessorImpl{
|
||||||
|
letterRepo: letterRepo,
|
||||||
|
approvalFlowRepo: approvalFlowRepo,
|
||||||
|
numberGenerator: numberGenerator,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *LetterCreationProcessorImpl) PrepareLetterForCreation(ctx context.Context, letter *entities.LetterOutgoing, departmentID uuid.UUID) error {
|
||||||
|
// Assign approval flow from department if not provided
|
||||||
|
if letter.ApprovalFlowID == nil && departmentID != uuid.Nil {
|
||||||
|
flow, err := p.approvalFlowRepo.GetByDepartment(ctx, departmentID)
|
||||||
|
if err == nil && flow != nil {
|
||||||
|
letter.ApprovalFlowID = &flow.ID
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set initial status based on approval flow
|
||||||
|
if letter.ApprovalFlowID != nil {
|
||||||
|
letter.Status = entities.LetterOutgoingStatusPendingApproval
|
||||||
|
} else {
|
||||||
|
letter.Status = entities.LetterOutgoingStatusApproved
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *LetterCreationProcessorImpl) CreateLetter(ctx context.Context, letter *entities.LetterOutgoing) error {
|
||||||
|
return p.letterRepo.Create(ctx, letter)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *LetterCreationProcessorImpl) GenerateLetterNumber(ctx context.Context, letter *entities.LetterOutgoing) error {
|
||||||
|
letterNumber, err := p.numberGenerator.GenerateNumber(
|
||||||
|
ctx,
|
||||||
|
"outgoing_letter_prefix",
|
||||||
|
"outgoing_letter_sequence",
|
||||||
|
"ESLO",
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
letter.LetterNumber = letterNumber
|
||||||
|
return nil
|
||||||
|
}
|
||||||
221
internal/processor/letter_outgoing_recipient_processor.go
Normal file
221
internal/processor/letter_outgoing_recipient_processor.go
Normal file
@ -0,0 +1,221 @@
|
|||||||
|
package processor
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"eslogad-be/internal/entities"
|
||||||
|
"eslogad-be/internal/repository"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
// LetterOutgoingRecipientProcessor handles all recipient-related operations for outgoing letters
|
||||||
|
type LetterOutgoingRecipientProcessor interface {
|
||||||
|
// CreateRecipients creates multiple recipients for a letter
|
||||||
|
CreateRecipients(ctx context.Context, letterID uuid.UUID, recipients []entities.LetterOutgoingRecipient) error
|
||||||
|
|
||||||
|
// CreateInitialRecipients creates recipients from both approval workflow and department members
|
||||||
|
CreateInitialRecipients(ctx context.Context, letter *entities.LetterOutgoing, creatorDepartmentID uuid.UUID) error
|
||||||
|
|
||||||
|
// UpdateRecipient updates a single recipient's information
|
||||||
|
UpdateRecipient(ctx context.Context, recipient *entities.LetterOutgoingRecipient) error
|
||||||
|
|
||||||
|
// RemoveRecipient removes a recipient from a letter
|
||||||
|
RemoveRecipient(ctx context.Context, letterID uuid.UUID, recipientID uuid.UUID) error
|
||||||
|
|
||||||
|
// GetRecipientsByLetterID retrieves all recipients for a specific letter
|
||||||
|
GetRecipientsByLetterID(ctx context.Context, letterID uuid.UUID) ([]entities.LetterOutgoingRecipient, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type LetterOutgoingRecipientProcessorImpl struct {
|
||||||
|
recipientRepo *repository.LetterOutgoingRecipientRepository
|
||||||
|
approvalFlowRepo *repository.ApprovalFlowRepository
|
||||||
|
userDeptRepo *repository.UserDepartmentRepository
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewLetterOutgoingRecipientProcessor(
|
||||||
|
recipientRepo *repository.LetterOutgoingRecipientRepository,
|
||||||
|
approvalFlowRepo *repository.ApprovalFlowRepository,
|
||||||
|
userDeptRepo *repository.UserDepartmentRepository,
|
||||||
|
) *LetterOutgoingRecipientProcessorImpl {
|
||||||
|
return &LetterOutgoingRecipientProcessorImpl{
|
||||||
|
recipientRepo: recipientRepo,
|
||||||
|
approvalFlowRepo: approvalFlowRepo,
|
||||||
|
userDeptRepo: userDeptRepo,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateRecipients creates multiple recipients for a letter
|
||||||
|
func (p *LetterOutgoingRecipientProcessorImpl) CreateRecipients(
|
||||||
|
ctx context.Context,
|
||||||
|
letterID uuid.UUID,
|
||||||
|
recipients []entities.LetterOutgoingRecipient,
|
||||||
|
) error {
|
||||||
|
if len(recipients) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure all recipients have the correct letter ID and default status
|
||||||
|
for i := range recipients {
|
||||||
|
recipients[i].LetterID = letterID
|
||||||
|
if recipients[i].Status == "" {
|
||||||
|
recipients[i].Status = "pending"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return p.recipientRepo.CreateBulk(ctx, recipients)
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateInitialRecipients creates the initial set of recipients for an outgoing letter
|
||||||
|
// It combines:
|
||||||
|
// 1. Approvers from the approval workflow (if exists)
|
||||||
|
// 2. All active users from the letter creator's department
|
||||||
|
func (p *LetterOutgoingRecipientProcessorImpl) CreateInitialRecipients(
|
||||||
|
ctx context.Context,
|
||||||
|
letter *entities.LetterOutgoing,
|
||||||
|
creatorDepartmentID uuid.UUID,
|
||||||
|
) error {
|
||||||
|
// Track unique users to avoid duplicates
|
||||||
|
uniqueUsers := make(map[uuid.UUID]bool)
|
||||||
|
var allRecipients []entities.LetterOutgoingRecipient
|
||||||
|
|
||||||
|
// Step 1: Add recipients from approval workflow
|
||||||
|
approvalRecipients := p.collectApprovalWorkflowRecipients(ctx, letter, uniqueUsers)
|
||||||
|
allRecipients = append(allRecipients, approvalRecipients...)
|
||||||
|
|
||||||
|
// Step 2: Add all users from the creator's department
|
||||||
|
departmentRecipients := p.collectDepartmentRecipients(ctx, letter.ID, creatorDepartmentID, uniqueUsers)
|
||||||
|
allRecipients = append(allRecipients, departmentRecipients...)
|
||||||
|
|
||||||
|
// Step 3: Mark the first recipient as primary and save all
|
||||||
|
if len(allRecipients) > 0 {
|
||||||
|
allRecipients[0].IsPrimary = true
|
||||||
|
return p.recipientRepo.CreateBulk(ctx, allRecipients)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// collectApprovalWorkflowRecipients gathers all users who are approvers in the workflow
|
||||||
|
func (p *LetterOutgoingRecipientProcessorImpl) collectApprovalWorkflowRecipients(
|
||||||
|
ctx context.Context,
|
||||||
|
letter *entities.LetterOutgoing,
|
||||||
|
existingUsers map[uuid.UUID]bool,
|
||||||
|
) []entities.LetterOutgoingRecipient {
|
||||||
|
var recipients []entities.LetterOutgoingRecipient
|
||||||
|
|
||||||
|
// If no approval workflow is assigned, return empty
|
||||||
|
if letter.ApprovalFlowID == nil {
|
||||||
|
return recipients
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch the approval workflow
|
||||||
|
approvalFlow, err := p.approvalFlowRepo.Get(ctx, *letter.ApprovalFlowID)
|
||||||
|
if err != nil || approvalFlow == nil || len(approvalFlow.Steps) == 0 {
|
||||||
|
return recipients
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add each approver as a recipient
|
||||||
|
for _, step := range approvalFlow.Steps {
|
||||||
|
if step.ApproverUserID == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
userID := *step.ApproverUserID
|
||||||
|
|
||||||
|
// Skip if user is already added
|
||||||
|
if existingUsers[userID] {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
existingUsers[userID] = true
|
||||||
|
|
||||||
|
recipient := entities.LetterOutgoingRecipient{
|
||||||
|
LetterID: letter.ID,
|
||||||
|
UserID: &userID,
|
||||||
|
IsPrimary: false,
|
||||||
|
Status: "pending",
|
||||||
|
}
|
||||||
|
|
||||||
|
recipients = append(recipients, recipient)
|
||||||
|
}
|
||||||
|
|
||||||
|
return recipients
|
||||||
|
}
|
||||||
|
|
||||||
|
// collectDepartmentRecipients gathers all active users from a specific department
|
||||||
|
func (p *LetterOutgoingRecipientProcessorImpl) collectDepartmentRecipients(
|
||||||
|
ctx context.Context,
|
||||||
|
letterID uuid.UUID,
|
||||||
|
departmentID uuid.UUID,
|
||||||
|
existingUsers map[uuid.UUID]bool,
|
||||||
|
) []entities.LetterOutgoingRecipient {
|
||||||
|
var recipients []entities.LetterOutgoingRecipient
|
||||||
|
|
||||||
|
// If no department specified, return empty
|
||||||
|
if departmentID == uuid.Nil {
|
||||||
|
return recipients
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch all active users in the department
|
||||||
|
userDepartmentMappings, err := p.userDeptRepo.ListActiveByDepartmentIDs(ctx, []uuid.UUID{departmentID})
|
||||||
|
if err != nil {
|
||||||
|
return recipients
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add each department user as a recipient
|
||||||
|
for _, mapping := range userDepartmentMappings {
|
||||||
|
// Skip if user is already added (e.g., from approval workflow)
|
||||||
|
if existingUsers[mapping.UserID] {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
existingUsers[mapping.UserID] = true
|
||||||
|
|
||||||
|
recipient := entities.LetterOutgoingRecipient{
|
||||||
|
LetterID: letterID,
|
||||||
|
UserID: &mapping.UserID,
|
||||||
|
DepartmentID: &mapping.DepartmentID,
|
||||||
|
IsPrimary: false,
|
||||||
|
Status: "pending",
|
||||||
|
}
|
||||||
|
|
||||||
|
recipients = append(recipients, recipient)
|
||||||
|
}
|
||||||
|
|
||||||
|
return recipients
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateRecipient updates an existing recipient's information
|
||||||
|
func (p *LetterOutgoingRecipientProcessorImpl) UpdateRecipient(
|
||||||
|
ctx context.Context,
|
||||||
|
recipient *entities.LetterOutgoingRecipient,
|
||||||
|
) error {
|
||||||
|
return p.recipientRepo.Update(ctx, recipient)
|
||||||
|
}
|
||||||
|
|
||||||
|
// RemoveRecipient removes a recipient from a letter
|
||||||
|
func (p *LetterOutgoingRecipientProcessorImpl) RemoveRecipient(
|
||||||
|
ctx context.Context,
|
||||||
|
letterID uuid.UUID,
|
||||||
|
recipientID uuid.UUID,
|
||||||
|
) error {
|
||||||
|
return p.recipientRepo.Delete(ctx, recipientID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetRecipientsByLetterID retrieves all recipients for a specific letter
|
||||||
|
func (p *LetterOutgoingRecipientProcessorImpl) GetRecipientsByLetterID(
|
||||||
|
ctx context.Context,
|
||||||
|
letterID uuid.UUID,
|
||||||
|
) ([]entities.LetterOutgoingRecipient, error) {
|
||||||
|
recipientMap, err := p.recipientRepo.ListByLetterIDs(ctx, []uuid.UUID{letterID})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if recipients, ok := recipientMap[letterID]; ok {
|
||||||
|
return recipients, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return []entities.LetterOutgoingRecipient{}, nil
|
||||||
|
}
|
||||||
71
internal/processor/letter_validation_processor.go
Normal file
71
internal/processor/letter_validation_processor.go
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
package processor
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
|
||||||
|
"eslogad-be/internal/entities"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
type LetterValidationProcessor interface {
|
||||||
|
ValidateCreateOutgoingLetter(ctx context.Context, letter *entities.LetterOutgoing) error
|
||||||
|
ValidateUpdateOutgoingLetter(ctx context.Context, letter *entities.LetterOutgoing, existingLetter *entities.LetterOutgoing) error
|
||||||
|
ValidateDeleteOutgoingLetter(ctx context.Context, letter *entities.LetterOutgoing) error
|
||||||
|
ValidateApprovalSubmission(ctx context.Context, letter *entities.LetterOutgoing) error
|
||||||
|
}
|
||||||
|
|
||||||
|
type LetterValidationProcessorImpl struct{}
|
||||||
|
|
||||||
|
func NewLetterValidationProcessor() *LetterValidationProcessorImpl {
|
||||||
|
return &LetterValidationProcessorImpl{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *LetterValidationProcessorImpl) ValidateCreateOutgoingLetter(ctx context.Context, letter *entities.LetterOutgoing) error {
|
||||||
|
if letter.Subject == "" {
|
||||||
|
return errors.New("letter subject is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
if letter.CreatedBy == uuid.Nil {
|
||||||
|
return errors.New("letter creator is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
if letter.IssueDate.IsZero() {
|
||||||
|
return errors.New("letter issue date is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *LetterValidationProcessorImpl) ValidateUpdateOutgoingLetter(ctx context.Context, letter *entities.LetterOutgoing, existingLetter *entities.LetterOutgoing) error {
|
||||||
|
if existingLetter.Status != entities.LetterOutgoingStatusDraft {
|
||||||
|
return errors.New("only draft letters can be updated")
|
||||||
|
}
|
||||||
|
|
||||||
|
if letter.Subject == "" {
|
||||||
|
return errors.New("letter subject cannot be empty")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *LetterValidationProcessorImpl) ValidateDeleteOutgoingLetter(ctx context.Context, letter *entities.LetterOutgoing) error {
|
||||||
|
if letter.Status != entities.LetterOutgoingStatusDraft {
|
||||||
|
return errors.New("only draft letters can be deleted")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *LetterValidationProcessorImpl) ValidateApprovalSubmission(ctx context.Context, letter *entities.LetterOutgoing) error {
|
||||||
|
if letter.Status != entities.LetterOutgoingStatusDraft {
|
||||||
|
return errors.New("only draft letters can be submitted for approval")
|
||||||
|
}
|
||||||
|
|
||||||
|
if letter.ApprovalFlowID == nil {
|
||||||
|
return errors.New("approval flow is required for submission")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@ -356,6 +356,26 @@ func (r *DepartmentRepository) GetChildren(ctx context.Context, parentPath strin
|
|||||||
return departments, nil
|
return departments, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *DepartmentRepository) GetAllDescendants(ctx context.Context, parentID uuid.UUID) ([]entities.Department, error) {
|
||||||
|
db := DBFromContext(ctx, r.db)
|
||||||
|
|
||||||
|
// First get the parent department to get its path
|
||||||
|
var parent entities.Department
|
||||||
|
if err := db.WithContext(ctx).First(&parent, "id = ?", parentID).Error; err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var departments []entities.Department
|
||||||
|
// Get all descendants using ltree
|
||||||
|
if err := db.WithContext(ctx).
|
||||||
|
Where("path <@ ? AND path != ?", parent.Path, parent.Path).
|
||||||
|
Order("path ASC").
|
||||||
|
Find(&departments).Error; err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return departments, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (r *DepartmentRepository) UpdateChildrenPaths(ctx context.Context, oldPath, newPath string) error {
|
func (r *DepartmentRepository) UpdateChildrenPaths(ctx context.Context, oldPath, newPath string) error {
|
||||||
db := DBFromContext(ctx, r.db)
|
db := DBFromContext(ctx, r.db)
|
||||||
// Use raw SQL for ltree path update
|
// Use raw SQL for ltree path update
|
||||||
|
|||||||
@ -53,31 +53,57 @@ type LetterOutgoingService interface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type LetterOutgoingServiceImpl struct {
|
type LetterOutgoingServiceImpl struct {
|
||||||
processor processor.LetterOutgoingProcessor
|
processor processor.LetterOutgoingProcessor
|
||||||
|
txManager *repository.TxManager
|
||||||
|
validationProcessor processor.LetterValidationProcessor
|
||||||
|
creationProcessor processor.LetterCreationProcessor
|
||||||
|
approvalProcessor processor.LetterApprovalProcessor
|
||||||
|
attachmentProcessor processor.LetterAttachmentProcessor
|
||||||
|
recipientProcessor processor.LetterOutgoingRecipientProcessor
|
||||||
|
activityProcessor processor.LetterActivityProcessor
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewLetterOutgoingService(processor processor.LetterOutgoingProcessor) *LetterOutgoingServiceImpl {
|
func NewLetterOutgoingService(
|
||||||
|
processor processor.LetterOutgoingProcessor,
|
||||||
|
txManager *repository.TxManager,
|
||||||
|
validationProcessor processor.LetterValidationProcessor,
|
||||||
|
creationProcessor processor.LetterCreationProcessor,
|
||||||
|
approvalProcessor processor.LetterApprovalProcessor,
|
||||||
|
attachmentProcessor processor.LetterAttachmentProcessor,
|
||||||
|
recipientProcessor processor.LetterOutgoingRecipientProcessor,
|
||||||
|
activityProcessor processor.LetterActivityProcessor,
|
||||||
|
) *LetterOutgoingServiceImpl {
|
||||||
return &LetterOutgoingServiceImpl{
|
return &LetterOutgoingServiceImpl{
|
||||||
processor: processor,
|
processor: processor,
|
||||||
|
txManager: txManager,
|
||||||
|
validationProcessor: validationProcessor,
|
||||||
|
creationProcessor: creationProcessor,
|
||||||
|
approvalProcessor: approvalProcessor,
|
||||||
|
attachmentProcessor: attachmentProcessor,
|
||||||
|
recipientProcessor: recipientProcessor,
|
||||||
|
activityProcessor: activityProcessor,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *LetterOutgoingServiceImpl) CreateOutgoingLetter(ctx context.Context, req *contract.CreateOutgoingLetterRequest) (*contract.OutgoingLetterResponse, error) {
|
func (s *LetterOutgoingServiceImpl) CreateOutgoingLetter(ctx context.Context, req *contract.CreateOutgoingLetterRequest) (*contract.OutgoingLetterResponse, error) {
|
||||||
departmentID := getDepartmentIDFromContext(ctx)
|
departmentID := getDepartmentIDFromContext(ctx)
|
||||||
|
userID := getUserIDFromContext(ctx)
|
||||||
|
|
||||||
|
// Create letter entity
|
||||||
letter := &entities.LetterOutgoing{
|
letter := &entities.LetterOutgoing{
|
||||||
Subject: req.Subject,
|
Subject: req.Subject,
|
||||||
Description: req.Description,
|
Description: req.Description,
|
||||||
PriorityID: req.PriorityID,
|
PriorityID: req.PriorityID,
|
||||||
ReceiverInstitutionID: req.ReceiverInstitutionID,
|
ReceiverInstitutionID: req.ReceiverInstitutionID,
|
||||||
IssueDate: req.IssueDate,
|
IssueDate: req.IssueDate,
|
||||||
CreatedBy: req.UserID,
|
CreatedBy: userID,
|
||||||
}
|
}
|
||||||
|
|
||||||
if req.ReferenceNumber != nil {
|
if req.ReferenceNumber != nil {
|
||||||
letter.ReferenceNumber = req.ReferenceNumber
|
letter.ReferenceNumber = req.ReferenceNumber
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Prepare attachments
|
||||||
var attachments []entities.LetterOutgoingAttachment
|
var attachments []entities.LetterOutgoingAttachment
|
||||||
if len(req.Attachments) > 0 {
|
if len(req.Attachments) > 0 {
|
||||||
attachments = make([]entities.LetterOutgoingAttachment, len(req.Attachments))
|
attachments = make([]entities.LetterOutgoingAttachment, len(req.Attachments))
|
||||||
@ -86,16 +112,57 @@ func (s *LetterOutgoingServiceImpl) CreateOutgoingLetter(ctx context.Context, re
|
|||||||
FileURL: a.FileURL,
|
FileURL: a.FileURL,
|
||||||
FileName: a.FileName,
|
FileName: a.FileName,
|
||||||
FileType: a.FileType,
|
FileType: a.FileType,
|
||||||
UploadedBy: &req.UserID,
|
UploadedBy: &userID,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
err := s.processor.CreateOutgoingLetter(ctx, letter, attachments, req.UserID, departmentID)
|
// Execute creation with transaction in service layer
|
||||||
|
err := s.txManager.WithTransaction(ctx, func(txCtx context.Context) error {
|
||||||
|
// Step 1: Validate letter
|
||||||
|
if err := s.validationProcessor.ValidateCreateOutgoingLetter(txCtx, letter); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step 2: Prepare letter for creation (assign approval flow, set initial status)
|
||||||
|
if err := s.creationProcessor.PrepareLetterForCreation(txCtx, letter, departmentID); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step 3: Generate letter number
|
||||||
|
if err := s.creationProcessor.GenerateLetterNumber(txCtx, letter); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step 4: Create the letter
|
||||||
|
if err := s.creationProcessor.CreateLetter(txCtx, letter); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step 5: Create approval steps if needed
|
||||||
|
if err := s.approvalProcessor.CreateApprovalSteps(txCtx, letter); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step 6: Create initial recipients (approval workflow users + department members)
|
||||||
|
if err := s.recipientProcessor.CreateInitialRecipients(txCtx, letter, departmentID); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step 7: Create attachments
|
||||||
|
if err := s.attachmentProcessor.CreateAttachments(txCtx, letter.ID, attachments); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step 8: Log the activity
|
||||||
|
return s.activityProcessor.LogActivity(txCtx, letter.ID, entities.LetterOutgoingActionCreated, userID, nil)
|
||||||
|
})
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Get the created letter with all relationships
|
||||||
result, err := s.processor.GetOutgoingLetterByID(ctx, letter.ID)
|
result, err := s.processor.GetOutgoingLetterByID(ctx, letter.ID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|||||||
15
migrations/000035_add_parent_department_id.down.sql
Normal file
15
migrations/000035_add_parent_department_id.down.sql
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
-- Drop the view
|
||||||
|
DROP VIEW IF EXISTS department_hierarchy;
|
||||||
|
|
||||||
|
-- Drop the functions
|
||||||
|
DROP FUNCTION IF EXISTS get_department_hierarchy_path(UUID);
|
||||||
|
DROP FUNCTION IF EXISTS check_department_hierarchy();
|
||||||
|
|
||||||
|
-- Drop the trigger
|
||||||
|
DROP TRIGGER IF EXISTS check_department_hierarchy_trigger ON departments;
|
||||||
|
|
||||||
|
-- Drop the index
|
||||||
|
DROP INDEX IF EXISTS idx_departments_parent_id;
|
||||||
|
|
||||||
|
-- Remove the parent_department_id column
|
||||||
|
ALTER TABLE departments DROP COLUMN IF EXISTS parent_department_id;
|
||||||
128
migrations/000035_add_parent_department_id.up.sql
Normal file
128
migrations/000035_add_parent_department_id.up.sql
Normal file
@ -0,0 +1,128 @@
|
|||||||
|
-- 1) Schema changes
|
||||||
|
ALTER TABLE departments
|
||||||
|
ADD COLUMN parent_department_id UUID REFERENCES departments(id) ON DELETE CASCADE;
|
||||||
|
|
||||||
|
CREATE INDEX idx_departments_parent_id ON departments(parent_department_id);
|
||||||
|
|
||||||
|
-- (Optional but recommended for ltree lookups)
|
||||||
|
-- CREATE EXTENSION IF NOT EXISTS ltree;
|
||||||
|
-- CREATE INDEX idx_departments_path_gist ON departments USING GIST (path);
|
||||||
|
-- CREATE INDEX idx_departments_path_nlevel ON departments ((nlevel(path)));
|
||||||
|
|
||||||
|
-- 2) Migrate parent ids from existing ltree path
|
||||||
|
-- Use ltree helpers: nlevel() and subpath()
|
||||||
|
UPDATE departments d1
|
||||||
|
SET parent_department_id = d2.id
|
||||||
|
FROM departments d2
|
||||||
|
WHERE nlevel(d1.path) > 1
|
||||||
|
AND d2.path = subpath(d1.path, 0, nlevel(d1.path) - 1);
|
||||||
|
|
||||||
|
-- 3) Guard against cycles/self-parent via trigger
|
||||||
|
CREATE OR REPLACE FUNCTION check_department_hierarchy()
|
||||||
|
RETURNS TRIGGER AS $$
|
||||||
|
DECLARE
|
||||||
|
current_id UUID;
|
||||||
|
max_depth INT := 100;
|
||||||
|
depth INT := 0;
|
||||||
|
BEGIN
|
||||||
|
IF NEW.parent_department_id IS NULL THEN
|
||||||
|
RETURN NEW;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
-- self-reference
|
||||||
|
IF NEW.parent_department_id = NEW.id THEN
|
||||||
|
RAISE EXCEPTION 'Department cannot be its own parent';
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
-- walk up the tree
|
||||||
|
current_id := NEW.parent_department_id;
|
||||||
|
WHILE current_id IS NOT NULL AND depth < max_depth LOOP
|
||||||
|
IF current_id = NEW.id THEN
|
||||||
|
RAISE EXCEPTION 'Circular reference detected in department hierarchy';
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
SELECT parent_department_id INTO current_id
|
||||||
|
FROM departments
|
||||||
|
WHERE id = current_id;
|
||||||
|
|
||||||
|
depth := depth + 1;
|
||||||
|
END LOOP;
|
||||||
|
|
||||||
|
IF depth >= max_depth THEN
|
||||||
|
RAISE EXCEPTION 'Department hierarchy too deep (max: %)', max_depth;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
RETURN NEW;
|
||||||
|
END;
|
||||||
|
$$ LANGUAGE plpgsql;
|
||||||
|
|
||||||
|
DROP TRIGGER IF EXISTS check_department_hierarchy_trigger ON departments;
|
||||||
|
|
||||||
|
CREATE TRIGGER check_department_hierarchy_trigger
|
||||||
|
BEFORE INSERT OR UPDATE OF parent_department_id ON departments
|
||||||
|
FOR EACH ROW
|
||||||
|
EXECUTE FUNCTION check_department_hierarchy();
|
||||||
|
|
||||||
|
-- 4) Helper to rebuild dotted text path from codes (top.down.leaf)
|
||||||
|
CREATE OR REPLACE FUNCTION get_department_hierarchy_path(dept_id UUID)
|
||||||
|
RETURNS TEXT AS $$
|
||||||
|
DECLARE
|
||||||
|
path_text TEXT := '';
|
||||||
|
current_dept RECORD;
|
||||||
|
current_id UUID := dept_id;
|
||||||
|
BEGIN
|
||||||
|
WHILE current_id IS NOT NULL LOOP
|
||||||
|
SELECT id, name, parent_department_id, code
|
||||||
|
INTO current_dept
|
||||||
|
FROM departments
|
||||||
|
WHERE id = current_id;
|
||||||
|
|
||||||
|
IF current_dept IS NULL THEN
|
||||||
|
EXIT;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
IF path_text = '' THEN
|
||||||
|
path_text := current_dept.code;
|
||||||
|
ELSE
|
||||||
|
path_text := current_dept.code || '.' || path_text;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
current_id := current_dept.parent_department_id;
|
||||||
|
END LOOP;
|
||||||
|
|
||||||
|
RETURN path_text;
|
||||||
|
END;
|
||||||
|
$$ LANGUAGE plpgsql;
|
||||||
|
|
||||||
|
-- 5) View for easy hierarchy queries
|
||||||
|
CREATE OR REPLACE VIEW department_hierarchy AS
|
||||||
|
WITH RECURSIVE dept_tree AS (
|
||||||
|
-- roots
|
||||||
|
SELECT
|
||||||
|
id,
|
||||||
|
name,
|
||||||
|
code,
|
||||||
|
parent_department_id,
|
||||||
|
path,
|
||||||
|
0 AS level,
|
||||||
|
ARRAY[id] AS hierarchy_ids,
|
||||||
|
ARRAY[name] AS hierarchy_names
|
||||||
|
FROM departments
|
||||||
|
WHERE parent_department_id IS NULL
|
||||||
|
|
||||||
|
UNION ALL
|
||||||
|
|
||||||
|
-- children
|
||||||
|
SELECT
|
||||||
|
d.id,
|
||||||
|
d.name,
|
||||||
|
d.code,
|
||||||
|
d.parent_department_id,
|
||||||
|
d.path,
|
||||||
|
dt.level + 1,
|
||||||
|
dt.hierarchy_ids || d.id,
|
||||||
|
dt.hierarchy_names || d.name
|
||||||
|
FROM departments d
|
||||||
|
JOIN dept_tree dt ON d.parent_department_id = dt.id
|
||||||
|
)
|
||||||
|
SELECT * FROM dept_tree;
|
||||||
Loading…
x
Reference in New Issue
Block a user