From e3cd04a90e60eeefca3cc7c5c04642be6c6c7caf Mon Sep 17 00:00:00 2001 From: Aditya Siregar Date: Mon, 13 Oct 2025 07:43:18 +0700 Subject: [PATCH] add archive --- internal/entities/letter_incoming.go | 2 + internal/entities/letter_outgoing.go | 4 +- internal/handler/letter_handler.go | 17 ++ .../processor/letter_outgoing_processor.go | 257 ++++++++++++------ internal/processor/letter_processor.go | 4 + .../repository/letter_outgoing_repository.go | 24 +- internal/repository/letter_repository.go | 28 +- internal/router/health_handler.go | 1 + internal/router/router.go | 1 + internal/service/letter_outgoing_service.go | 6 +- internal/service/letter_service.go | 13 +- ...chive_columns_to_letters_outgoing.down.sql | 11 + ...archive_columns_to_letters_outgoing.up.sql | 12 + ...chive_columns_to_letters_incoming.down.sql | 11 + ...archive_columns_to_letters_incoming.up.sql | 12 + 15 files changed, 296 insertions(+), 107 deletions(-) create mode 100644 migrations/000040_add_archive_columns_to_letters_outgoing.down.sql create mode 100644 migrations/000040_add_archive_columns_to_letters_outgoing.up.sql create mode 100644 migrations/000041_add_archive_columns_to_letters_incoming.down.sql create mode 100644 migrations/000041_add_archive_columns_to_letters_incoming.up.sql diff --git a/internal/entities/letter_incoming.go b/internal/entities/letter_incoming.go index 09335c2..893832e 100644 --- a/internal/entities/letter_incoming.go +++ b/internal/entities/letter_incoming.go @@ -35,6 +35,8 @@ type LetterIncoming struct { DueDate *time.Time `json:"due_date,omitempty"` Type LetterIncomingType `gorm:"not null;default:'UTAMA'" json:"type"` Status LetterIncomingStatus `gorm:"not null;default:'new'" json:"status"` + IsArchived bool `gorm:"not null;default:false" json:"is_archived"` + ArchivedAt *time.Time `json:"archived_at,omitempty"` CreatedBy uuid.UUID `gorm:"not null" json:"created_by"` CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"` UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"` diff --git a/internal/entities/letter_outgoing.go b/internal/entities/letter_outgoing.go index 6d81244..1bbbcee 100644 --- a/internal/entities/letter_outgoing.go +++ b/internal/entities/letter_outgoing.go @@ -13,7 +13,7 @@ const ( LetterOutgoingStatusPendingApproval LetterOutgoingStatus = "pending_approval" LetterOutgoingStatusApproved LetterOutgoingStatus = "approved" LetterOutgoingStatusSent LetterOutgoingStatus = "sent" - LetterOutgoingStatusArchived LetterOutgoingStatus = "archived" + LetterOutgoingStatusRejected LetterOutgoingStatus = "rejected" ) type LetterOutgoing struct { @@ -29,6 +29,8 @@ type LetterOutgoing struct { Status LetterOutgoingStatus `gorm:"not null;default:'draft'" json:"status"` ApprovalFlowID *uuid.UUID `json:"approval_flow_id,omitempty"` CreatedBy uuid.UUID `gorm:"not null" json:"created_by"` + IsArchived bool `gorm:"not null;default:false" json:"is_archived"` + ArchivedAt *time.Time `json:"archived_at,omitempty"` CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"` UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"` DeletedAt *time.Time `gorm:"index" json:"deleted_at,omitempty"` diff --git a/internal/handler/letter_handler.go b/internal/handler/letter_handler.go index 116915c..7ea20e7 100644 --- a/internal/handler/letter_handler.go +++ b/internal/handler/letter_handler.go @@ -24,6 +24,7 @@ type LetterService interface { UpdateIncomingLetter(ctx context.Context, id uuid.UUID, req *contract.UpdateIncomingLetterRequest) (*contract.IncomingLetterResponse, error) SoftDeleteIncomingLetter(ctx context.Context, id uuid.UUID) error BulkArchiveIncomingLetters(ctx context.Context, letterIDs []uuid.UUID) (*contract.BulkArchiveLettersResponse, error) + ArchiveIncomingLetter(ctx context.Context, letterID uuid.UUID) error CreateDispositions(ctx context.Context, req *contract.CreateLetterDispositionRequest) (*contract.ListDispositionsResponse, error) GetEnhancedDispositionsByLetter(ctx context.Context, letterID uuid.UUID) (*contract.ListEnhancedDispositionsResponse, error) @@ -465,3 +466,19 @@ func (h *LetterHandler) BulkArchiveIncomingLetters(c *gin.Context) { h.respondSuccess(c, http.StatusOK, resp) } + +func (h *LetterHandler) ArchiveIncomingLetter(c *gin.Context) { + id, err := uuid.Parse(c.Param("id")) + if err != nil { + h.respondError(c, http.StatusBadRequest, "invalid id") + return + } + + err = h.svc.ArchiveIncomingLetter(c.Request.Context(), id) + if err != nil { + h.handleServiceError(c, err) + return + } + + h.respondSuccess(c, http.StatusOK, &contract.SuccessResponse{Message: "archived"}) +} diff --git a/internal/processor/letter_outgoing_processor.go b/internal/processor/letter_outgoing_processor.go index c11755a..7128df0 100644 --- a/internal/processor/letter_outgoing_processor.go +++ b/internal/processor/letter_outgoing_processor.go @@ -21,6 +21,7 @@ type LetterOutgoingProcessor interface { DeleteOutgoingLetter(ctx context.Context, id uuid.UUID, userID uuid.UUID) error UpdateLetterStatus(ctx context.Context, letterID uuid.UUID, status entities.LetterOutgoingStatus, userID uuid.UUID, fromStatus, toStatus *string) error + ArchiveOutgoingLetter(ctx context.Context, letterID uuid.UUID, userID uuid.UUID) error ProcessApprovalSubmission(ctx context.Context, letterID uuid.UUID, approvalFlowID uuid.UUID, userID uuid.UUID) error ProcessApproval(ctx context.Context, letterID uuid.UUID, approval *entities.LetterOutgoingApproval, userID uuid.UUID, allApproved bool) error @@ -45,14 +46,13 @@ type LetterOutgoingProcessor interface { GetOutgoingLetterWithDetails(ctx context.Context, letterID uuid.UUID) (*entities.LetterOutgoing, error) GetUsersByIDs(ctx context.Context, userIDs []uuid.UUID) ([]entities.User, error) BulkArchiveOutgoingLetters(ctx context.Context, letterIDs []uuid.UUID) (int64, error) - + // Batch loading methods for efficient querying GetBatchAttachments(ctx context.Context, letterIDs []uuid.UUID) (map[uuid.UUID][]entities.LetterOutgoingAttachment, error) GetBatchRecipients(ctx context.Context, letterIDs []uuid.UUID) (map[uuid.UUID][]entities.LetterOutgoingRecipient, error) GetBatchPriorities(ctx context.Context, priorityIDs []uuid.UUID) (map[uuid.UUID]*entities.Priority, error) GetBatchInstitutions(ctx context.Context, institutionIDs []uuid.UUID) (map[uuid.UUID]*entities.Institution, error) GetBatchOutgoingRecipientsByUser(ctx context.Context, letterIDs []uuid.UUID, userID uuid.UUID) (map[uuid.UUID]*entities.LetterOutgoingRecipient, error) - } type LetterOutgoingProcessorImpl struct { @@ -356,8 +356,6 @@ func (p *LetterOutgoingProcessorImpl) UpdateLetterStatus(ctx context.Context, le activityLog.ActionType = entities.LetterOutgoingActionApproved case entities.LetterOutgoingStatusSent: activityLog.ActionType = entities.LetterOutgoingActionSent - case entities.LetterOutgoingStatusArchived: - activityLog.ActionType = entities.LetterOutgoingActionArchived default: activityLog.ActionType = entities.LetterOutgoingActionUpdated } @@ -370,6 +368,28 @@ func (p *LetterOutgoingProcessorImpl) UpdateLetterStatus(ctx context.Context, le }) } +func (p *LetterOutgoingProcessorImpl) ArchiveOutgoingLetter(ctx context.Context, letterID uuid.UUID, userID uuid.UUID) error { + return p.txManager.WithTransaction(ctx, func(txCtx context.Context) error { + // Archive the letter using the new flag + if err := p.letterRepo.Archive(txCtx, letterID); err != nil { + return err + } + + // Log the activity + activityLog := &entities.LetterOutgoingActivityLog{ + LetterID: letterID, + ActorUserID: &userID, + ActionType: entities.LetterOutgoingActionArchived, + } + + if err := p.activityLogRepo.Create(txCtx, activityLog); err != nil { + return err + } + + return nil + }) +} + func (p *LetterOutgoingProcessorImpl) ProcessApprovalSubmission(ctx context.Context, letterID uuid.UUID, approvalFlowID uuid.UUID, userID uuid.UUID) error { flow, err := p.approvalFlowRepo.Get(ctx, approvalFlowID) if err != nil { @@ -465,104 +485,173 @@ func (p *LetterOutgoingProcessorImpl) ProcessApprovalSubmission(ctx context.Cont func (p *LetterOutgoingProcessorImpl) ProcessApproval(ctx context.Context, letterID uuid.UUID, approval *entities.LetterOutgoingApproval, userID uuid.UUID, allApproved bool) error { return p.txManager.WithTransaction(ctx, func(txCtx context.Context) error { - now := time.Now() - approval.Status = entities.ApprovalStatusApproved - approval.ApproverID = &userID - approval.ActedAt = &now - - if err := p.approvalRepo.Update(txCtx, approval); err != nil { + // Step 1: Update the approval record + if err := p.updateApprovalRecord(txCtx, approval, userID); err != nil { return err } - allApprovals, err := p.approvalRepo.ListByLetter(txCtx, letterID) + // Step 2: Get all approvals and organize by step + approvalsByStep, err := p.getApprovalsByStep(txCtx, letterID) if err != nil { return err } - approvalsByStep := make(map[int][]entities.LetterOutgoingApproval) - for _, a := range allApprovals { - approvalsByStep[a.StepOrder] = append(approvalsByStep[a.StepOrder], a) - } - - currentStepCompleted := true - for _, a := range approvalsByStep[approval.StepOrder] { - if a.IsRequired && a.Status != entities.ApprovalStatusApproved { - currentStepCompleted = false - break + // Step 3: Check if current step is completed + if p.isStepCompleted(approvalsByStep[approval.StepOrder]) { + // Step 4: Activate next step if exists + if err := p.activateNextStep(txCtx, letterID, approval.StepOrder, approvalsByStep); err != nil { + return err } } - // If current step is completed, activate the next step and add approvers as recipients - if currentStepCompleted { - nextStepOrder := approval.StepOrder + 1 - if nextStepApprovals, exists := approvalsByStep[nextStepOrder]; exists { - currentRecipients, err := p.recipientRepo.ListByLetter(txCtx, letterID) - if err != nil { - return err - } - - existingUserIDs := make(map[uuid.UUID]bool) - for _, recipient := range currentRecipients { - if recipient.UserID != nil { - existingUserIDs[*recipient.UserID] = true - } - } - - for _, nextApproval := range nextStepApprovals { - - if nextApproval.Status == entities.ApprovalStatusNotStarted { - nextApproval.Status = entities.ApprovalStatusPending - if err := p.approvalRepo.Update(txCtx, &nextApproval); err != nil { - return err - } - } - - if nextApproval.ApproverID != nil && !existingUserIDs[*nextApproval.ApproverID] { - newRecipient := entities.LetterOutgoingRecipient{ - LetterID: letterID, - UserID: nextApproval.ApproverID, - IsPrimary: false, - Status: "unread", - IsArchived: false, - } - if err := p.recipientRepo.Create(txCtx, &newRecipient); err != nil { - return err - } - - existingUserIDs[*nextApproval.ApproverID] = true - } - } - } - } - - // Check if all required approvals are completed - allRequiredApproved := true - for _, a := range allApprovals { - if a.IsRequired && a.Status != entities.ApprovalStatusApproved { - allRequiredApproved = false - break - } - } - - // Update letter status if all required approvals are done - if allRequiredApproved { + // Step 5: Check if all required approvals are completed + if p.areAllRequiredApprovalsCompleted(approvalsByStep) { + // Step 6: Update letter status to approved if err := p.letterRepo.UpdateStatus(txCtx, letterID, entities.LetterOutgoingStatusApproved); err != nil { return err } } - activityLog := &entities.LetterOutgoingActivityLog{ - LetterID: letterID, - ActionType: entities.LetterOutgoingActionApproved, - ActorUserID: &userID, - TargetID: &approval.ID, + // Step 7: Log the activity + return p.logApprovalActivity(txCtx, letterID, approval.ID, userID) + }) +} + +// updateApprovalRecord updates the approval with approver info and timestamp +func (p *LetterOutgoingProcessorImpl) updateApprovalRecord(ctx context.Context, approval *entities.LetterOutgoingApproval, userID uuid.UUID) error { + now := time.Now() + approval.Status = entities.ApprovalStatusApproved + approval.ApproverID = &userID + approval.ActedAt = &now + + return p.approvalRepo.Update(ctx, approval) +} + +// getApprovalsByStep fetches all approvals and organizes them by step order +func (p *LetterOutgoingProcessorImpl) getApprovalsByStep(ctx context.Context, letterID uuid.UUID) (map[int][]entities.LetterOutgoingApproval, error) { + allApprovals, err := p.approvalRepo.ListByLetter(ctx, letterID) + if err != nil { + return nil, err + } + + approvalsByStep := make(map[int][]entities.LetterOutgoingApproval) + for _, approval := range allApprovals { + approvalsByStep[approval.StepOrder] = append(approvalsByStep[approval.StepOrder], approval) + } + + return approvalsByStep, nil +} + +// isStepCompleted checks if all required approvals in a step are approved +func (p *LetterOutgoingProcessorImpl) isStepCompleted(stepApprovals []entities.LetterOutgoingApproval) bool { + for _, approval := range stepApprovals { + if approval.IsRequired && approval.Status != entities.ApprovalStatusApproved { + return false } - if err := p.activityLogRepo.Create(txCtx, activityLog); err != nil { + } + return true +} + +// activateNextStep activates the next approval step and adds approvers as recipients +func (p *LetterOutgoingProcessorImpl) activateNextStep(ctx context.Context, letterID uuid.UUID, currentStepOrder int, approvalsByStep map[int][]entities.LetterOutgoingApproval) error { + nextStepOrder := currentStepOrder + 1 + nextStepApprovals, exists := approvalsByStep[nextStepOrder] + if !exists { + return nil // No next step + } + + // Get existing recipients to avoid duplicates + existingUserIDs, err := p.getExistingRecipientUserIDs(ctx, letterID) + if err != nil { + return err + } + + // Process each approval in the next step + for _, nextApproval := range nextStepApprovals { + // Activate approval if not started + 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 +} + +// getExistingRecipientUserIDs gets a set of existing recipient user IDs +func (p *LetterOutgoingProcessorImpl) getExistingRecipientUserIDs(ctx context.Context, letterID uuid.UUID) (map[uuid.UUID]bool, error) { + currentRecipients, err := p.recipientRepo.ListByLetter(ctx, letterID) + if err != nil { + return nil, err + } + + existingUserIDs := make(map[uuid.UUID]bool) + for _, recipient := range currentRecipients { + if recipient.UserID != nil { + existingUserIDs[*recipient.UserID] = true + } + } + + return existingUserIDs, nil +} + +// activateApprovalIfNotStarted changes approval status from not_started to pending +func (p *LetterOutgoingProcessorImpl) activateApprovalIfNotStarted(ctx context.Context, approval *entities.LetterOutgoingApproval) error { + if approval.Status != entities.ApprovalStatusNotStarted { return nil - }) + } + + approval.Status = entities.ApprovalStatusPending + return p.approvalRepo.Update(ctx, approval) +} + +// addApproverAsRecipientIfNeeded adds an approver as a recipient if they don't exist +func (p *LetterOutgoingProcessorImpl) addApproverAsRecipientIfNeeded(ctx context.Context, letterID uuid.UUID, approverID *uuid.UUID, existingUserIDs map[uuid.UUID]bool) error { + if approverID == nil || existingUserIDs[*approverID] { + return nil + } + + newRecipient := entities.LetterOutgoingRecipient{ + LetterID: letterID, + UserID: approverID, + IsPrimary: false, + Status: "unread", + IsArchived: false, + } + + if err := p.recipientRepo.Create(ctx, &newRecipient); err != nil { + return err + } + + existingUserIDs[*approverID] = true + return nil +} + +// areAllRequiredApprovalsCompleted checks if all required approvals are completed +func (p *LetterOutgoingProcessorImpl) areAllRequiredApprovalsCompleted(approvalsByStep map[int][]entities.LetterOutgoingApproval) bool { + for _, stepApprovals := range approvalsByStep { + for _, approval := range stepApprovals { + if approval.IsRequired && approval.Status != entities.ApprovalStatusApproved { + return false + } + } + } + return true +} + +// logApprovalActivity creates an activity log for the approval action +func (p *LetterOutgoingProcessorImpl) logApprovalActivity(ctx context.Context, letterID, approvalID uuid.UUID, userID uuid.UUID) error { + activityLog := &entities.LetterOutgoingActivityLog{ + LetterID: letterID, + ActionType: entities.LetterOutgoingActionApproved, + ActorUserID: &userID, + TargetID: &approvalID, + } + return p.activityLogRepo.Create(ctx, activityLog) } func (p *LetterOutgoingProcessorImpl) ProcessRejection(ctx context.Context, letterID uuid.UUID, approval *entities.LetterOutgoingApproval, userID uuid.UUID) error { @@ -576,12 +665,12 @@ func (p *LetterOutgoingProcessorImpl) ProcessRejection(ctx context.Context, lett return err } - if err := p.letterRepo.UpdateStatus(txCtx, letterID, entities.LetterOutgoingStatusDraft); err != nil { + if err := p.letterRepo.UpdateStatus(txCtx, letterID, entities.LetterOutgoingStatusRejected); err != nil { return err } fromStatus := string(entities.LetterOutgoingStatusPendingApproval) - toStatus := string(entities.LetterOutgoingStatusDraft) + toStatus := string(entities.LetterOutgoingStatusRejected) activityLog := &entities.LetterOutgoingActivityLog{ LetterID: letterID, ActionType: entities.LetterOutgoingActionRejected, diff --git a/internal/processor/letter_processor.go b/internal/processor/letter_processor.go index 17ec986..3d9fb82 100644 --- a/internal/processor/letter_processor.go +++ b/internal/processor/letter_processor.go @@ -623,6 +623,10 @@ func (p *LetterProcessorImpl) BulkArchiveIncomingLetters(ctx context.Context, le return p.letterRepo.BulkArchive(ctx, letterIDs) } +func (p *LetterProcessorImpl) ArchiveIncomingLetter(ctx context.Context, letterID uuid.UUID) error { + return p.letterRepo.Archive(ctx, letterID) +} + // BulkArchiveIncomingLettersForUser archives letters for a specific user only func (p *LetterProcessorImpl) BulkArchiveIncomingLettersForUser(ctx context.Context, letterIDs []uuid.UUID, userID uuid.UUID) (int64, error) { return p.letterRepo.BulkArchiveForUser(ctx, letterIDs, userID) diff --git a/internal/repository/letter_outgoing_repository.go b/internal/repository/letter_outgoing_repository.go index 6813893..a31e1a4 100644 --- a/internal/repository/letter_outgoing_repository.go +++ b/internal/repository/letter_outgoing_repository.go @@ -54,13 +54,29 @@ func (r *LetterOutgoingRepository) SoftDelete(ctx context.Context, id uuid.UUID) func (r *LetterOutgoingRepository) BulkArchive(ctx context.Context, letterIDs []uuid.UUID) (int64, error) { db := DBFromContext(ctx, r.db) + now := time.Now() result := db.WithContext(ctx). Model(&entities.LetterOutgoing{}). Where("id IN ? AND deleted_at IS NULL", letterIDs). - Update("status", "archived") + Updates(map[string]interface{}{ + "is_archived": true, + "archived_at": now, + }) return result.RowsAffected, result.Error } +func (r *LetterOutgoingRepository) Archive(ctx context.Context, letterID uuid.UUID) error { + db := DBFromContext(ctx, r.db) + now := time.Now() + return db.WithContext(ctx). + Model(&entities.LetterOutgoing{}). + Where("id = ? AND deleted_at IS NULL", letterID). + Updates(map[string]interface{}{ + "is_archived": true, + "archived_at": now, + }).Error +} + func (r *LetterOutgoingRepository) GetWithRelations(ctx context.Context, id uuid.UUID, relations []string) (*entities.LetterOutgoing, error) { db := DBFromContext(ctx, r.db) query := db.WithContext(ctx).Where("id = ? AND deleted_at IS NULL", id) @@ -103,11 +119,7 @@ func (r *LetterOutgoingRepository) List(ctx context.Context, filter ListOutgoing // Apply is_archived filter if filter.IsArchived != nil { - if *filter.IsArchived { - query = query.Where("letters_outgoing.status = 'archived'") - } else { - query = query.Where("letters_outgoing.status != 'archived'") - } + query = query.Where("letters_outgoing.is_archived = ?", *filter.IsArchived) } if filter.Status != nil { diff --git a/internal/repository/letter_repository.go b/internal/repository/letter_repository.go index 415e4da..71a2da8 100644 --- a/internal/repository/letter_repository.go +++ b/internal/repository/letter_repository.go @@ -45,15 +45,29 @@ func (r *LetterIncomingRepository) SoftDelete(ctx context.Context, id uuid.UUID) func (r *LetterIncomingRepository) BulkArchive(ctx context.Context, letterIDs []uuid.UUID) (int64, error) { db := DBFromContext(ctx, r.db) - // For incoming letters, we archive the recipients, not the letter itself - // The letter status remains as is (new, in_progress, or completed) + now := time.Now() result := db.WithContext(ctx). - Model(&entities.LetterIncomingRecipient{}). - Where("letter_id IN ?", letterIDs). - Update("is_archived", true) + Model(&entities.LetterIncoming{}). + Where("id IN ? AND deleted_at IS NULL", letterIDs). + Updates(map[string]interface{}{ + "is_archived": true, + "archived_at": now, + }) return result.RowsAffected, result.Error } +func (r *LetterIncomingRepository) Archive(ctx context.Context, letterID uuid.UUID) error { + db := DBFromContext(ctx, r.db) + now := time.Now() + return db.WithContext(ctx). + Model(&entities.LetterIncoming{}). + Where("id = ? AND deleted_at IS NULL", letterID). + Updates(map[string]interface{}{ + "is_archived": true, + "archived_at": now, + }).Error +} + // BulkArchiveForUser archives letters for a specific user only func (r *LetterIncomingRepository) BulkArchiveForUser(ctx context.Context, letterIDs []uuid.UUID, userID uuid.UUID) (int64, error) { db := DBFromContext(ctx, r.db) @@ -122,9 +136,9 @@ func (r *LetterIncomingRepository) List(ctx context.Context, filter ListIncoming if filter.IsArchived != nil { if *filter.IsArchived { - query = query.Where("letter_incoming_recipients.is_archived = ?", true) + query = query.Where("letters_incoming.is_archived = ?", true) } else { - query = query.Where("letter_incoming_recipients.is_archived = ? OR letter_incoming_recipients.is_archived IS NULL", false) + query = query.Where("letters_incoming.is_archived = ? OR letters_incoming.is_archived IS NULL", false) } } diff --git a/internal/router/health_handler.go b/internal/router/health_handler.go index 645cc29..eca4276 100644 --- a/internal/router/health_handler.go +++ b/internal/router/health_handler.go @@ -83,6 +83,7 @@ type LetterHandler interface { UpdateIncomingLetter(c *gin.Context) DeleteIncomingLetter(c *gin.Context) BulkArchiveIncomingLetters(c *gin.Context) + ArchiveIncomingLetter(c *gin.Context) CreateDispositions(c *gin.Context) GetEnhancedDispositionsByLetter(c *gin.Context) diff --git a/internal/router/router.go b/internal/router/router.go index 36cf1ab..831abf7 100644 --- a/internal/router/router.go +++ b/internal/router/router.go @@ -179,6 +179,7 @@ func (r *Router) addAppRoutes(rg *gin.Engine) { lettersch.PUT("/incoming/:id/read", r.letterHandler.MarkIncomingLetterAsRead) lettersch.DELETE("/incoming/:id", r.letterHandler.DeleteIncomingLetter) lettersch.POST("/incoming/archive", r.letterHandler.BulkArchiveIncomingLetters) + lettersch.PUT("/incoming/:id/archive", r.letterHandler.ArchiveIncomingLetter) lettersch.POST("/outgoing", r.letterOutgoingHandler.CreateOutgoingLetter) lettersch.GET("/outgoing/search", r.letterOutgoingHandler.SearchOutgoingLetters) diff --git a/internal/service/letter_outgoing_service.go b/internal/service/letter_outgoing_service.go index c5e01c6..2a22237 100644 --- a/internal/service/letter_outgoing_service.go +++ b/internal/service/letter_outgoing_service.go @@ -750,13 +750,13 @@ func (s *LetterOutgoingServiceImpl) ArchiveOutgoingLetter(ctx context.Context, l return err } + // Can only archive sent letters if letter.Status != entities.LetterOutgoingStatusSent { return gorm.ErrInvalidData } - fromStatus := string(entities.LetterOutgoingStatusSent) - toStatus := string(entities.LetterOutgoingStatusArchived) - return s.processor.UpdateLetterStatus(ctx, letterID, entities.LetterOutgoingStatusArchived, userID, &fromStatus, &toStatus) + // Use the new archive method instead of changing status + return s.processor.ArchiveOutgoingLetter(ctx, letterID, userID) } func (s *LetterOutgoingServiceImpl) AddRecipients(ctx context.Context, letterID uuid.UUID, req *contract.AddRecipientsRequest) error { diff --git a/internal/service/letter_service.go b/internal/service/letter_service.go index b93063d..7ec599f 100644 --- a/internal/service/letter_service.go +++ b/internal/service/letter_service.go @@ -32,6 +32,7 @@ type LetterProcessor interface { UpdateIncomingLetter(ctx context.Context, id uuid.UUID, req *contract.UpdateIncomingLetterRequest) (*contract.IncomingLetterResponse, error) SoftDeleteIncomingLetter(ctx context.Context, id uuid.UUID) error BulkArchiveIncomingLetters(ctx context.Context, letterIDs []uuid.UUID) (int64, error) + ArchiveIncomingLetter(ctx context.Context, letterID uuid.UUID) error BulkArchiveIncomingLettersForUser(ctx context.Context, letterIDs []uuid.UUID, userID uuid.UUID) (int64, error) // Batch loading methods @@ -816,12 +817,8 @@ func (s *LetterServiceImpl) GetLetterCTA(ctx context.Context, letterID uuid.UUID } func (s *LetterServiceImpl) BulkArchiveIncomingLetters(ctx context.Context, letterIDs []uuid.UUID) (*contract.BulkArchiveLettersResponse, error) { - // Extract user context to archive only for the current user - appCtx := appcontext.FromGinContext(ctx) - userID := appCtx.UserID - - // Archive letters only for the current user - archivedCount, err := s.processor.BulkArchiveIncomingLettersForUser(ctx, letterIDs, userID) + // Archive the letters themselves + archivedCount, err := s.processor.BulkArchiveIncomingLetters(ctx, letterIDs) if err != nil { return nil, err } @@ -833,6 +830,10 @@ func (s *LetterServiceImpl) BulkArchiveIncomingLetters(ctx context.Context, lett }, nil } +func (s *LetterServiceImpl) ArchiveIncomingLetter(ctx context.Context, letterID uuid.UUID) error { + return s.processor.ArchiveIncomingLetter(ctx, letterID) +} + func (s *LetterServiceImpl) sendDiscussionMentionNotifications(ctx context.Context, letterID uuid.UUID, senderUserID uuid.UUID, mentions map[string]interface{}, message string) { // Extract user_ids from mentions userIDs := s.extractUserIDsFromMentions(mentions) diff --git a/migrations/000040_add_archive_columns_to_letters_outgoing.down.sql b/migrations/000040_add_archive_columns_to_letters_outgoing.down.sql new file mode 100644 index 0000000..f5cf477 --- /dev/null +++ b/migrations/000040_add_archive_columns_to_letters_outgoing.down.sql @@ -0,0 +1,11 @@ +BEGIN; + +DROP INDEX IF EXISTS idx_letters_outgoing_is_archived; +DROP INDEX IF EXISTS idx_letters_outgoing_archived_at; +DROP INDEX IF EXISTS idx_letters_outgoing_archived_status; + +ALTER TABLE letters_outgoing +DROP COLUMN IF EXISTS is_archived, +DROP COLUMN IF EXISTS archived_at; + +COMMIT; \ No newline at end of file diff --git a/migrations/000040_add_archive_columns_to_letters_outgoing.up.sql b/migrations/000040_add_archive_columns_to_letters_outgoing.up.sql new file mode 100644 index 0000000..bf657b3 --- /dev/null +++ b/migrations/000040_add_archive_columns_to_letters_outgoing.up.sql @@ -0,0 +1,12 @@ +BEGIN; + +ALTER TABLE letters_outgoing +ADD COLUMN IF NOT EXISTS is_archived BOOLEAN NOT NULL DEFAULT false, +ADD COLUMN IF NOT EXISTS archived_at TIMESTAMP WITHOUT TIME ZONE; + +CREATE INDEX IF NOT EXISTS idx_letters_outgoing_is_archived ON letters_outgoing(is_archived); +CREATE INDEX IF NOT EXISTS idx_letters_outgoing_archived_at ON letters_outgoing(archived_at); + +CREATE INDEX IF NOT EXISTS idx_letters_outgoing_archived_status ON letters_outgoing(is_archived, status); + +COMMIT; \ No newline at end of file diff --git a/migrations/000041_add_archive_columns_to_letters_incoming.down.sql b/migrations/000041_add_archive_columns_to_letters_incoming.down.sql new file mode 100644 index 0000000..efb58dc --- /dev/null +++ b/migrations/000041_add_archive_columns_to_letters_incoming.down.sql @@ -0,0 +1,11 @@ +BEGIN; + +DROP INDEX IF EXISTS idx_letters_incoming_is_archived; +DROP INDEX IF EXISTS idx_letters_incoming_archived_at; +DROP INDEX IF EXISTS idx_letters_incoming_archived_status; + +ALTER TABLE letters_incoming +DROP COLUMN IF EXISTS is_archived, +DROP COLUMN IF EXISTS archived_at; + +COMMIT; \ No newline at end of file diff --git a/migrations/000041_add_archive_columns_to_letters_incoming.up.sql b/migrations/000041_add_archive_columns_to_letters_incoming.up.sql new file mode 100644 index 0000000..4aad3c3 --- /dev/null +++ b/migrations/000041_add_archive_columns_to_letters_incoming.up.sql @@ -0,0 +1,12 @@ +BEGIN; + +ALTER TABLE letters_incoming +ADD COLUMN IF NOT EXISTS is_archived BOOLEAN NOT NULL DEFAULT false, +ADD COLUMN IF NOT EXISTS archived_at TIMESTAMP WITHOUT TIME ZONE; + +CREATE INDEX IF NOT EXISTS idx_letters_incoming_is_archived ON letters_incoming(is_archived); +CREATE INDEX IF NOT EXISTS idx_letters_incoming_archived_at ON letters_incoming(archived_at); + +CREATE INDEX IF NOT EXISTS idx_letters_incoming_archived_status ON letters_incoming(is_archived, status); + +COMMIT; \ No newline at end of file