Add Surat Keluar

This commit is contained in:
Aditya Siregar
2025-09-20 16:31:22 +07:00
parent 0399c87736
commit e4946d6e05
13 changed files with 1151 additions and 13 deletions
+270
View 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
}
@@ -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
}