Add Surat Keluar
This commit is contained in:
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user