diff --git a/internal/processor/letter_outgoing_processor.go b/internal/processor/letter_outgoing_processor.go index bf66325..6ca0755 100644 --- a/internal/processor/letter_outgoing_processor.go +++ b/internal/processor/letter_outgoing_processor.go @@ -17,6 +17,7 @@ import ( type LetterOutgoingProcessor interface { CreateOutgoingLetter(ctx context.Context, letter *entities.LetterOutgoing, attachments []entities.LetterOutgoingAttachment, userID, departmentID uuid.UUID) error GetOutgoingLetterByID(ctx context.Context, id uuid.UUID) (*entities.LetterOutgoing, error) + GetOutgoingLetterByReferenceNumber(ctx context.Context, referenceNumber *string) (*entities.LetterOutgoing, error) ListOutgoingLetters(ctx context.Context, filter repository.ListOutgoingLettersFilter, limit, offset int) ([]entities.LetterOutgoing, int64, error) SearchOutgoingLetters(ctx context.Context, filters map[string]interface{}, page, limit int, sortBy, sortOrder string) ([]entities.LetterOutgoing, int64, error) UpdateOutgoingLetter(ctx context.Context, letter *entities.LetterOutgoing, userID uuid.UUID) error @@ -110,6 +111,11 @@ func NewLetterOutgoingProcessor( } func (p *LetterOutgoingProcessorImpl) CreateOutgoingLetter(ctx context.Context, letter *entities.LetterOutgoing, attachments []entities.LetterOutgoingAttachment, userID, departmentID uuid.UUID) error { + existingOutgoing, err := p.letterRepo.GetByReferenceNumber(ctx, letter.ReferenceNumber) + if err == nil && existingOutgoing != nil { + return fmt.Errorf("surat dengan nomor %s sudah ada", *letter.ReferenceNumber) + } + return p.txManager.WithTransaction(ctx, func(txCtx context.Context) error { // Step 1: Assign approval flow from department if not provided if err := p.assignApprovalFlowFromDepartment(txCtx, letter, departmentID); err != nil { @@ -148,6 +154,10 @@ func (p *LetterOutgoingProcessorImpl) CreateOutgoingLetter(ctx context.Context, }) } +func (p *LetterOutgoingProcessorImpl) GetOutgoingLetterByReferenceNumber(ctx context.Context, referenceNumber *string) (*entities.LetterOutgoing, error) { + return p.letterRepo.GetByReferenceNumber(ctx, referenceNumber) +} + func (p *LetterOutgoingProcessorImpl) assignApprovalFlowFromDepartment(ctx context.Context, letter *entities.LetterOutgoing, departmentID uuid.UUID) error { if letter.ApprovalFlowID != nil || departmentID == uuid.Nil { return nil @@ -598,17 +608,17 @@ func (p *LetterOutgoingProcessorImpl) isStepCompleted(stepApprovals []entities.L approvalsByParallelGroup := make(map[int][]entities.LetterOutgoingApproval) for _, approval := range stepApprovals { approvalsByParallelGroup[approval.ParallelGroup] = append( - approvalsByParallelGroup[approval.ParallelGroup], + approvalsByParallelGroup[approval.ParallelGroup], approval, ) } - + // Check each parallel group for _, groupApprovals := range approvalsByParallelGroup { // For each parallel group, check if it has at least one approved groupHasApproval := false hasRequiredPending := false - + for _, approval := range groupApprovals { if approval.Status == entities.ApprovalStatusApproved { groupHasApproval = true @@ -617,12 +627,12 @@ func (p *LetterOutgoingProcessorImpl) isStepCompleted(stepApprovals []entities.L hasRequiredPending = true } } - + // If this group has required approvals that are still pending, step is not complete if hasRequiredPending { return false } - + // If this group has no approvals at all and contains required approvals, step is not complete if !groupHasApproval { for _, approval := range groupApprovals { @@ -632,7 +642,7 @@ func (p *LetterOutgoingProcessorImpl) isStepCompleted(stepApprovals []entities.L } } } - + return true } @@ -778,7 +788,7 @@ func (p *LetterOutgoingProcessorImpl) activateNextParallelGroupForRevision(ctx c groups = append(groups, group) } sort.Ints(groups) - + var nextGroup int = -1 for _, group := range groups { if group > currentGroup { @@ -786,22 +796,22 @@ func (p *LetterOutgoingProcessorImpl) activateNextParallelGroupForRevision(ctx c break } } - + if nextGroup == -1 { return nil // No next group } - + nextGroupApprovals, exists := approvalsByGroup[nextGroup] if !exists { return nil } - + // Get existing recipients to avoid duplicates existingUserIDs, err := p.getExistingRecipientUserIDs(ctx, letterID) if err != nil { return err } - + // Process each approval in the next group for _, nextApproval := range nextGroupApprovals { // Only process if it's the same revision @@ -810,14 +820,14 @@ func (p *LetterOutgoingProcessorImpl) activateNextParallelGroupForRevision(ctx c if err := p.activateApprovalIfNotStarted(ctx, &nextApproval); err != nil { return err } - + // Add approver as recipient if not already exists if err := p.addApproverAsRecipientIfNeeded(ctx, letterID, nextApproval.ApproverID, existingUserIDs); err != nil { return err } } } - + return nil } diff --git a/internal/processor/letter_processor.go b/internal/processor/letter_processor.go index 97aac5b..d8d268b 100644 --- a/internal/processor/letter_processor.go +++ b/internal/processor/letter_processor.go @@ -60,6 +60,11 @@ func NewLetterProcessor(letterRepo *repository.LetterIncomingRepository, attachR func (p *LetterProcessorImpl) CreateIncomingLetter(ctx context.Context, req *contract.CreateIncomingLetterRequest) (*contract.IncomingLetterResponse, error) { userID := appcontext.FromGinContext(ctx).UserID + existingIncoming, err := p.letterRepo.GetByReferenceNumber(ctx, req.ReferenceNumber) + if err == nil && existingIncoming != nil { + return nil, fmt.Errorf("surat dengan nomor %s sudah ada", *req.ReferenceNumber) + } + letterType := entities.LetterIncomingTypeUtama if req.Type == "TEMBUSAN" { letterType = entities.LetterIncomingTypeTembusan diff --git a/internal/repository/letter_outgoing_repository.go b/internal/repository/letter_outgoing_repository.go index 6236480..e5df114 100644 --- a/internal/repository/letter_outgoing_repository.go +++ b/internal/repository/letter_outgoing_repository.go @@ -41,6 +41,25 @@ func (r *LetterOutgoingRepository) Get(ctx context.Context, id uuid.UUID) (*enti return &e, nil } +func (r *LetterOutgoingRepository) GetByReferenceNumber(ctx context.Context, refNumber *string) (*entities.LetterOutgoing, error) { + db := DBFromContext(ctx, r.db) + var e entities.LetterOutgoing + if err := db.WithContext(ctx). + Preload("Priority"). + Preload("ReceiverInstitution"). + Preload("Creator"). + Preload("ApprovalFlow"). + Preload("Recipients"). + Preload("Attachments"). + Preload("Approvals.Step"). + Preload("Approvals.Approver"). + Where("reference_number = ? AND deleted_at IS NULL", refNumber). + First(&e).Error; err != nil { + return nil, err + } + return &e, nil +} + func (r *LetterOutgoingRepository) Update(ctx context.Context, e *entities.LetterOutgoing) error { db := DBFromContext(ctx, r.db) return db.WithContext(ctx).Model(&entities.LetterOutgoing{}).Where("id = ? AND deleted_at IS NULL", e.ID).Updates(e).Error diff --git a/internal/repository/letter_repository.go b/internal/repository/letter_repository.go index f73746e..03f3177 100644 --- a/internal/repository/letter_repository.go +++ b/internal/repository/letter_repository.go @@ -29,6 +29,17 @@ func (r *LetterIncomingRepository) Get(ctx context.Context, id uuid.UUID) (*enti return &e, nil } +func (r *LetterIncomingRepository) GetByReferenceNumber(ctx context.Context, refNumber *string) (*entities.LetterIncoming, error) { + db := DBFromContext(ctx, r.db) + var e entities.LetterIncoming + if err := db.WithContext(ctx). + Where("reference_number = ? AND deleted_at IS NULL", refNumber). + First(&e).Error; err != nil { + return nil, err + } + return &e, nil +} + func (r *LetterIncomingRepository) GetByID(ctx context.Context, id uuid.UUID) (*entities.LetterIncoming, error) { return r.Get(ctx, id) } diff --git a/internal/service/letter_outgoing_service.go b/internal/service/letter_outgoing_service.go index e4a2afe..24f01d4 100644 --- a/internal/service/letter_outgoing_service.go +++ b/internal/service/letter_outgoing_service.go @@ -94,6 +94,12 @@ func (s *LetterOutgoingServiceImpl) CreateOutgoingLetter(ctx context.Context, re departmentID := getDepartmentIDFromContext(ctx) userID := getUserIDFromContext(ctx) + existingOutgoing, err := s.processor.GetOutgoingLetterByReferenceNumber(ctx, req.ReferenceNumber) + + if err == nil && existingOutgoing != nil { + return nil, fmt.Errorf("surat dengan nomor %s sudah ada", *req.ReferenceNumber) + } + // Create letter entity letter := &entities.LetterOutgoing{ Subject: req.Subject, @@ -125,7 +131,7 @@ func (s *LetterOutgoingServiceImpl) CreateOutgoingLetter(ctx context.Context, re } // Execute creation with transaction in service layer - err := s.txManager.WithTransaction(ctx, func(txCtx context.Context) error { + 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 @@ -660,7 +666,7 @@ func (s *LetterOutgoingServiceImpl) ApproveOutgoingLetter(ctx context.Context, l if s.notificationProcessor != nil { // Get next parallel group to determine notification message nextParallelGroup := s.getNextParallelGroup(approvals, currentApproval.ParallelGroup) - + if nextParallelGroup > 0 { // Notify creator about group completion AND next group approvers creatorMessage := fmt.Sprintf("Surat keluar '%s' telah disetujui pada grup %d, menunggu persetujuan grup berikutnya", letter.Subject, currentApproval.ParallelGroup) @@ -1896,14 +1902,14 @@ func (s *LetterOutgoingServiceImpl) getNextParallelGroup(approvals []entities.Le for _, approval := range approvals { groupsMap[approval.ParallelGroup] = true } - + // Convert to sorted slice var groups []int for group := range groupsMap { groups = append(groups, group) } sort.Ints(groups) - + // Find the next group after current for _, group := range groups { if group > currentGroup { @@ -1915,45 +1921,45 @@ func (s *LetterOutgoingServiceImpl) getNextParallelGroup(approvals []entities.Le } } } - + return 0 // No next group } // Send notifications to approvers in a specific parallel group func (s *LetterOutgoingServiceImpl) sendParallelGroupApprovalNotifications(ctx context.Context, letterID uuid.UUID, subject string, parallelGroup int) { log.Printf("[DEBUG] sendParallelGroupApprovalNotifications START - LetterID: %s, ParallelGroup: %d", letterID.String(), parallelGroup) - + // Get the letter to know the current revision letter, err := s.processor.GetOutgoingLetterByID(ctx, letterID) if err != nil { log.Printf("[ERROR] Failed to get letter: %v", err) return } - + // Get approvals for the current revision only approvals, err := s.processor.GetApprovalsByLetterAndRevision(ctx, letterID, letter.RevisionNumber) if err != nil { log.Printf("[ERROR] Failed to get approvals: %v", err) return } - + log.Printf("[DEBUG] Found %d approvals", len(approvals)) - + // Find approvers for the specified parallel group for _, approval := range approvals { log.Printf("[DEBUG] Checking approval: ParallelGroup=%d, Status=%s, ApproverID=%v", approval.ParallelGroup, approval.Status, approval.ApproverID) - + if approval.ParallelGroup == parallelGroup && approval.ApproverID != nil { log.Printf("[DEBUG] Sending notification to approver %s for parallel group %d", approval.ApproverID.String(), parallelGroup) - + err := s.notificationProcessor.SendOutgoingLetterNotification( ctx, letterID, *approval.ApproverID, "Surat Keluar Perlu Persetujuan", fmt.Sprintf("Surat keluar '%s' memerlukan persetujuan Anda pada grup %d", subject, parallelGroup)) - + if err != nil { log.Printf("[ERROR] Failed to send notification to approver %s: %v", approval.ApproverID.String(), err) } else {