update unique letter number

This commit is contained in:
efrilm 2025-10-20 22:50:08 +07:00
parent 0c3e1db502
commit 88ad35c5d0
5 changed files with 76 additions and 25 deletions

View File

@ -17,6 +17,7 @@ import (
type LetterOutgoingProcessor interface { type LetterOutgoingProcessor interface {
CreateOutgoingLetter(ctx context.Context, letter *entities.LetterOutgoing, attachments []entities.LetterOutgoingAttachment, userID, departmentID uuid.UUID) error 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) 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) 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) 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 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 { 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 { return p.txManager.WithTransaction(ctx, func(txCtx context.Context) error {
// Step 1: Assign approval flow from department if not provided // Step 1: Assign approval flow from department if not provided
if err := p.assignApprovalFlowFromDepartment(txCtx, letter, departmentID); err != nil { 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 { func (p *LetterOutgoingProcessorImpl) assignApprovalFlowFromDepartment(ctx context.Context, letter *entities.LetterOutgoing, departmentID uuid.UUID) error {
if letter.ApprovalFlowID != nil || departmentID == uuid.Nil { if letter.ApprovalFlowID != nil || departmentID == uuid.Nil {
return nil return nil
@ -598,17 +608,17 @@ func (p *LetterOutgoingProcessorImpl) isStepCompleted(stepApprovals []entities.L
approvalsByParallelGroup := make(map[int][]entities.LetterOutgoingApproval) approvalsByParallelGroup := make(map[int][]entities.LetterOutgoingApproval)
for _, approval := range stepApprovals { for _, approval := range stepApprovals {
approvalsByParallelGroup[approval.ParallelGroup] = append( approvalsByParallelGroup[approval.ParallelGroup] = append(
approvalsByParallelGroup[approval.ParallelGroup], approvalsByParallelGroup[approval.ParallelGroup],
approval, approval,
) )
} }
// Check each parallel group // Check each parallel group
for _, groupApprovals := range approvalsByParallelGroup { for _, groupApprovals := range approvalsByParallelGroup {
// For each parallel group, check if it has at least one approved // For each parallel group, check if it has at least one approved
groupHasApproval := false groupHasApproval := false
hasRequiredPending := false hasRequiredPending := false
for _, approval := range groupApprovals { for _, approval := range groupApprovals {
if approval.Status == entities.ApprovalStatusApproved { if approval.Status == entities.ApprovalStatusApproved {
groupHasApproval = true groupHasApproval = true
@ -617,12 +627,12 @@ func (p *LetterOutgoingProcessorImpl) isStepCompleted(stepApprovals []entities.L
hasRequiredPending = true hasRequiredPending = true
} }
} }
// If this group has required approvals that are still pending, step is not complete // If this group has required approvals that are still pending, step is not complete
if hasRequiredPending { if hasRequiredPending {
return false return false
} }
// If this group has no approvals at all and contains required approvals, step is not complete // If this group has no approvals at all and contains required approvals, step is not complete
if !groupHasApproval { if !groupHasApproval {
for _, approval := range groupApprovals { for _, approval := range groupApprovals {
@ -632,7 +642,7 @@ func (p *LetterOutgoingProcessorImpl) isStepCompleted(stepApprovals []entities.L
} }
} }
} }
return true return true
} }
@ -778,7 +788,7 @@ func (p *LetterOutgoingProcessorImpl) activateNextParallelGroupForRevision(ctx c
groups = append(groups, group) groups = append(groups, group)
} }
sort.Ints(groups) sort.Ints(groups)
var nextGroup int = -1 var nextGroup int = -1
for _, group := range groups { for _, group := range groups {
if group > currentGroup { if group > currentGroup {
@ -786,22 +796,22 @@ func (p *LetterOutgoingProcessorImpl) activateNextParallelGroupForRevision(ctx c
break break
} }
} }
if nextGroup == -1 { if nextGroup == -1 {
return nil // No next group return nil // No next group
} }
nextGroupApprovals, exists := approvalsByGroup[nextGroup] nextGroupApprovals, exists := approvalsByGroup[nextGroup]
if !exists { if !exists {
return nil return nil
} }
// Get existing recipients to avoid duplicates // Get existing recipients to avoid duplicates
existingUserIDs, err := p.getExistingRecipientUserIDs(ctx, letterID) existingUserIDs, err := p.getExistingRecipientUserIDs(ctx, letterID)
if err != nil { if err != nil {
return err return err
} }
// Process each approval in the next group // Process each approval in the next group
for _, nextApproval := range nextGroupApprovals { for _, nextApproval := range nextGroupApprovals {
// Only process if it's the same revision // 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 { if err := p.activateApprovalIfNotStarted(ctx, &nextApproval); err != nil {
return err return err
} }
// Add approver as recipient if not already exists // Add approver as recipient if not already exists
if err := p.addApproverAsRecipientIfNeeded(ctx, letterID, nextApproval.ApproverID, existingUserIDs); err != nil { if err := p.addApproverAsRecipientIfNeeded(ctx, letterID, nextApproval.ApproverID, existingUserIDs); err != nil {
return err return err
} }
} }
} }
return nil return nil
} }

View File

@ -60,6 +60,11 @@ func NewLetterProcessor(letterRepo *repository.LetterIncomingRepository, attachR
func (p *LetterProcessorImpl) CreateIncomingLetter(ctx context.Context, req *contract.CreateIncomingLetterRequest) (*contract.IncomingLetterResponse, error) { func (p *LetterProcessorImpl) CreateIncomingLetter(ctx context.Context, req *contract.CreateIncomingLetterRequest) (*contract.IncomingLetterResponse, error) {
userID := appcontext.FromGinContext(ctx).UserID 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 letterType := entities.LetterIncomingTypeUtama
if req.Type == "TEMBUSAN" { if req.Type == "TEMBUSAN" {
letterType = entities.LetterIncomingTypeTembusan letterType = entities.LetterIncomingTypeTembusan

View File

@ -41,6 +41,25 @@ func (r *LetterOutgoingRepository) Get(ctx context.Context, id uuid.UUID) (*enti
return &e, nil 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 { func (r *LetterOutgoingRepository) Update(ctx context.Context, e *entities.LetterOutgoing) error {
db := DBFromContext(ctx, r.db) db := DBFromContext(ctx, r.db)
return db.WithContext(ctx).Model(&entities.LetterOutgoing{}).Where("id = ? AND deleted_at IS NULL", e.ID).Updates(e).Error return db.WithContext(ctx).Model(&entities.LetterOutgoing{}).Where("id = ? AND deleted_at IS NULL", e.ID).Updates(e).Error

View File

@ -29,6 +29,17 @@ func (r *LetterIncomingRepository) Get(ctx context.Context, id uuid.UUID) (*enti
return &e, nil 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) { func (r *LetterIncomingRepository) GetByID(ctx context.Context, id uuid.UUID) (*entities.LetterIncoming, error) {
return r.Get(ctx, id) return r.Get(ctx, id)
} }

View File

@ -94,6 +94,12 @@ func (s *LetterOutgoingServiceImpl) CreateOutgoingLetter(ctx context.Context, re
departmentID := getDepartmentIDFromContext(ctx) departmentID := getDepartmentIDFromContext(ctx)
userID := getUserIDFromContext(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 // Create letter entity
letter := &entities.LetterOutgoing{ letter := &entities.LetterOutgoing{
Subject: req.Subject, Subject: req.Subject,
@ -125,7 +131,7 @@ func (s *LetterOutgoingServiceImpl) CreateOutgoingLetter(ctx context.Context, re
} }
// Execute creation with transaction in service layer // 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 // Step 1: Validate letter
if err := s.validationProcessor.ValidateCreateOutgoingLetter(txCtx, letter); err != nil { if err := s.validationProcessor.ValidateCreateOutgoingLetter(txCtx, letter); err != nil {
return err return err
@ -660,7 +666,7 @@ func (s *LetterOutgoingServiceImpl) ApproveOutgoingLetter(ctx context.Context, l
if s.notificationProcessor != nil { if s.notificationProcessor != nil {
// Get next parallel group to determine notification message // Get next parallel group to determine notification message
nextParallelGroup := s.getNextParallelGroup(approvals, currentApproval.ParallelGroup) nextParallelGroup := s.getNextParallelGroup(approvals, currentApproval.ParallelGroup)
if nextParallelGroup > 0 { if nextParallelGroup > 0 {
// Notify creator about group completion AND next group approvers // 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) 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 { for _, approval := range approvals {
groupsMap[approval.ParallelGroup] = true groupsMap[approval.ParallelGroup] = true
} }
// Convert to sorted slice // Convert to sorted slice
var groups []int var groups []int
for group := range groupsMap { for group := range groupsMap {
groups = append(groups, group) groups = append(groups, group)
} }
sort.Ints(groups) sort.Ints(groups)
// Find the next group after current // Find the next group after current
for _, group := range groups { for _, group := range groups {
if group > currentGroup { if group > currentGroup {
@ -1915,45 +1921,45 @@ func (s *LetterOutgoingServiceImpl) getNextParallelGroup(approvals []entities.Le
} }
} }
} }
return 0 // No next group return 0 // No next group
} }
// Send notifications to approvers in a specific parallel group // Send notifications to approvers in a specific parallel group
func (s *LetterOutgoingServiceImpl) sendParallelGroupApprovalNotifications(ctx context.Context, letterID uuid.UUID, subject string, parallelGroup int) { 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) log.Printf("[DEBUG] sendParallelGroupApprovalNotifications START - LetterID: %s, ParallelGroup: %d", letterID.String(), parallelGroup)
// Get the letter to know the current revision // Get the letter to know the current revision
letter, err := s.processor.GetOutgoingLetterByID(ctx, letterID) letter, err := s.processor.GetOutgoingLetterByID(ctx, letterID)
if err != nil { if err != nil {
log.Printf("[ERROR] Failed to get letter: %v", err) log.Printf("[ERROR] Failed to get letter: %v", err)
return return
} }
// Get approvals for the current revision only // Get approvals for the current revision only
approvals, err := s.processor.GetApprovalsByLetterAndRevision(ctx, letterID, letter.RevisionNumber) approvals, err := s.processor.GetApprovalsByLetterAndRevision(ctx, letterID, letter.RevisionNumber)
if err != nil { if err != nil {
log.Printf("[ERROR] Failed to get approvals: %v", err) log.Printf("[ERROR] Failed to get approvals: %v", err)
return return
} }
log.Printf("[DEBUG] Found %d approvals", len(approvals)) log.Printf("[DEBUG] Found %d approvals", len(approvals))
// Find approvers for the specified parallel group // Find approvers for the specified parallel group
for _, approval := range approvals { for _, approval := range approvals {
log.Printf("[DEBUG] Checking approval: ParallelGroup=%d, Status=%s, ApproverID=%v", log.Printf("[DEBUG] Checking approval: ParallelGroup=%d, Status=%s, ApproverID=%v",
approval.ParallelGroup, approval.Status, approval.ApproverID) approval.ParallelGroup, approval.Status, approval.ApproverID)
if approval.ParallelGroup == parallelGroup && approval.ApproverID != nil { if approval.ParallelGroup == parallelGroup && approval.ApproverID != nil {
log.Printf("[DEBUG] Sending notification to approver %s for parallel group %d", approval.ApproverID.String(), parallelGroup) log.Printf("[DEBUG] Sending notification to approver %s for parallel group %d", approval.ApproverID.String(), parallelGroup)
err := s.notificationProcessor.SendOutgoingLetterNotification( err := s.notificationProcessor.SendOutgoingLetterNotification(
ctx, ctx,
letterID, letterID,
*approval.ApproverID, *approval.ApproverID,
"Surat Keluar Perlu Persetujuan", "Surat Keluar Perlu Persetujuan",
fmt.Sprintf("Surat keluar '%s' memerlukan persetujuan Anda pada grup %d", subject, parallelGroup)) fmt.Sprintf("Surat keluar '%s' memerlukan persetujuan Anda pada grup %d", subject, parallelGroup))
if err != nil { if err != nil {
log.Printf("[ERROR] Failed to send notification to approver %s: %v", approval.ApproverID.String(), err) log.Printf("[ERROR] Failed to send notification to approver %s: %v", approval.ApproverID.String(), err)
} else { } else {