add archive

This commit is contained in:
Aditya Siregar 2025-10-13 07:43:18 +07:00
parent b81c0be3c4
commit e3cd04a90e
15 changed files with 296 additions and 107 deletions

View File

@ -35,6 +35,8 @@ type LetterIncoming struct {
DueDate *time.Time `json:"due_date,omitempty"` DueDate *time.Time `json:"due_date,omitempty"`
Type LetterIncomingType `gorm:"not null;default:'UTAMA'" json:"type"` Type LetterIncomingType `gorm:"not null;default:'UTAMA'" json:"type"`
Status LetterIncomingStatus `gorm:"not null;default:'new'" json:"status"` 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"` CreatedBy uuid.UUID `gorm:"not null" json:"created_by"`
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"` CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"` UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"`

View File

@ -13,7 +13,7 @@ const (
LetterOutgoingStatusPendingApproval LetterOutgoingStatus = "pending_approval" LetterOutgoingStatusPendingApproval LetterOutgoingStatus = "pending_approval"
LetterOutgoingStatusApproved LetterOutgoingStatus = "approved" LetterOutgoingStatusApproved LetterOutgoingStatus = "approved"
LetterOutgoingStatusSent LetterOutgoingStatus = "sent" LetterOutgoingStatusSent LetterOutgoingStatus = "sent"
LetterOutgoingStatusArchived LetterOutgoingStatus = "archived" LetterOutgoingStatusRejected LetterOutgoingStatus = "rejected"
) )
type LetterOutgoing struct { type LetterOutgoing struct {
@ -29,6 +29,8 @@ type LetterOutgoing struct {
Status LetterOutgoingStatus `gorm:"not null;default:'draft'" json:"status"` Status LetterOutgoingStatus `gorm:"not null;default:'draft'" json:"status"`
ApprovalFlowID *uuid.UUID `json:"approval_flow_id,omitempty"` ApprovalFlowID *uuid.UUID `json:"approval_flow_id,omitempty"`
CreatedBy uuid.UUID `gorm:"not null" json:"created_by"` 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"` CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"` UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"`
DeletedAt *time.Time `gorm:"index" json:"deleted_at,omitempty"` DeletedAt *time.Time `gorm:"index" json:"deleted_at,omitempty"`

View File

@ -24,6 +24,7 @@ type LetterService interface {
UpdateIncomingLetter(ctx context.Context, id uuid.UUID, req *contract.UpdateIncomingLetterRequest) (*contract.IncomingLetterResponse, error) UpdateIncomingLetter(ctx context.Context, id uuid.UUID, req *contract.UpdateIncomingLetterRequest) (*contract.IncomingLetterResponse, error)
SoftDeleteIncomingLetter(ctx context.Context, id uuid.UUID) error SoftDeleteIncomingLetter(ctx context.Context, id uuid.UUID) error
BulkArchiveIncomingLetters(ctx context.Context, letterIDs []uuid.UUID) (*contract.BulkArchiveLettersResponse, 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) CreateDispositions(ctx context.Context, req *contract.CreateLetterDispositionRequest) (*contract.ListDispositionsResponse, error)
GetEnhancedDispositionsByLetter(ctx context.Context, letterID uuid.UUID) (*contract.ListEnhancedDispositionsResponse, 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) 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"})
}

View File

@ -21,6 +21,7 @@ type LetterOutgoingProcessor interface {
DeleteOutgoingLetter(ctx context.Context, id uuid.UUID, userID uuid.UUID) error 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 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 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 ProcessApproval(ctx context.Context, letterID uuid.UUID, approval *entities.LetterOutgoingApproval, userID uuid.UUID, allApproved bool) error
@ -52,7 +53,6 @@ type LetterOutgoingProcessor interface {
GetBatchPriorities(ctx context.Context, priorityIDs []uuid.UUID) (map[uuid.UUID]*entities.Priority, 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) 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) GetBatchOutgoingRecipientsByUser(ctx context.Context, letterIDs []uuid.UUID, userID uuid.UUID) (map[uuid.UUID]*entities.LetterOutgoingRecipient, error)
} }
type LetterOutgoingProcessorImpl struct { type LetterOutgoingProcessorImpl struct {
@ -356,8 +356,6 @@ func (p *LetterOutgoingProcessorImpl) UpdateLetterStatus(ctx context.Context, le
activityLog.ActionType = entities.LetterOutgoingActionApproved activityLog.ActionType = entities.LetterOutgoingActionApproved
case entities.LetterOutgoingStatusSent: case entities.LetterOutgoingStatusSent:
activityLog.ActionType = entities.LetterOutgoingActionSent activityLog.ActionType = entities.LetterOutgoingActionSent
case entities.LetterOutgoingStatusArchived:
activityLog.ActionType = entities.LetterOutgoingActionArchived
default: default:
activityLog.ActionType = entities.LetterOutgoingActionUpdated 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 { func (p *LetterOutgoingProcessorImpl) ProcessApprovalSubmission(ctx context.Context, letterID uuid.UUID, approvalFlowID uuid.UUID, userID uuid.UUID) error {
flow, err := p.approvalFlowRepo.Get(ctx, approvalFlowID) flow, err := p.approvalFlowRepo.Get(ctx, approvalFlowID)
if err != nil { if err != nil {
@ -465,42 +485,110 @@ 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 { 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 { return p.txManager.WithTransaction(ctx, func(txCtx context.Context) error {
// Step 1: Update the approval record
if err := p.updateApprovalRecord(txCtx, approval, userID); err != nil {
return err
}
// Step 2: Get all approvals and organize by step
approvalsByStep, err := p.getApprovalsByStep(txCtx, letterID)
if err != nil {
return err
}
// 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
}
}
// 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
}
}
// 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() now := time.Now()
approval.Status = entities.ApprovalStatusApproved approval.Status = entities.ApprovalStatusApproved
approval.ApproverID = &userID approval.ApproverID = &userID
approval.ActedAt = &now approval.ActedAt = &now
if err := p.approvalRepo.Update(txCtx, approval); err != nil { return p.approvalRepo.Update(ctx, approval)
return err }
}
allApprovals, err := p.approvalRepo.ListByLetter(txCtx, letterID) // 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 { if err != nil {
return err return nil, err
} }
approvalsByStep := make(map[int][]entities.LetterOutgoingApproval) approvalsByStep := make(map[int][]entities.LetterOutgoingApproval)
for _, a := range allApprovals { for _, approval := range allApprovals {
approvalsByStep[a.StepOrder] = append(approvalsByStep[a.StepOrder], a) approvalsByStep[approval.StepOrder] = append(approvalsByStep[approval.StepOrder], approval)
} }
currentStepCompleted := true return approvalsByStep, nil
for _, a := range approvalsByStep[approval.StepOrder] { }
if a.IsRequired && a.Status != entities.ApprovalStatusApproved {
currentStepCompleted = false // isStepCompleted checks if all required approvals in a step are approved
break func (p *LetterOutgoingProcessorImpl) isStepCompleted(stepApprovals []entities.LetterOutgoingApproval) bool {
for _, approval := range stepApprovals {
if approval.IsRequired && approval.Status != entities.ApprovalStatusApproved {
return false
} }
} }
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
}
// If current step is completed, activate the next step and add approvers as recipients // Get existing recipients to avoid duplicates
if currentStepCompleted { existingUserIDs, err := p.getExistingRecipientUserIDs(ctx, letterID)
nextStepOrder := approval.StepOrder + 1
if nextStepApprovals, exists := approvalsByStep[nextStepOrder]; exists {
currentRecipients, err := p.recipientRepo.ListByLetter(txCtx, letterID)
if err != nil { if err != nil {
return err 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) existingUserIDs := make(map[uuid.UUID]bool)
for _, recipient := range currentRecipients { for _, recipient := range currentRecipients {
if recipient.UserID != nil { if recipient.UserID != nil {
@ -508,61 +596,62 @@ func (p *LetterOutgoingProcessorImpl) ProcessApproval(ctx context.Context, lette
} }
} }
for _, nextApproval := range nextStepApprovals { return existingUserIDs, nil
}
if nextApproval.Status == entities.ApprovalStatusNotStarted { // activateApprovalIfNotStarted changes approval status from not_started to pending
nextApproval.Status = entities.ApprovalStatusPending func (p *LetterOutgoingProcessorImpl) activateApprovalIfNotStarted(ctx context.Context, approval *entities.LetterOutgoingApproval) error {
if err := p.approvalRepo.Update(txCtx, &nextApproval); err != nil { if approval.Status != entities.ApprovalStatusNotStarted {
return err 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
} }
if nextApproval.ApproverID != nil && !existingUserIDs[*nextApproval.ApproverID] {
newRecipient := entities.LetterOutgoingRecipient{ newRecipient := entities.LetterOutgoingRecipient{
LetterID: letterID, LetterID: letterID,
UserID: nextApproval.ApproverID, UserID: approverID,
IsPrimary: false, IsPrimary: false,
Status: "unread", Status: "unread",
IsArchived: false, IsArchived: false,
} }
if err := p.recipientRepo.Create(txCtx, &newRecipient); err != nil {
if err := p.recipientRepo.Create(ctx, &newRecipient); err != nil {
return err return err
} }
existingUserIDs[*nextApproval.ApproverID] = true existingUserIDs[*approverID] = true
} return nil
} }
}
}
// Check if all required approvals are completed // areAllRequiredApprovalsCompleted checks if all required approvals are completed
allRequiredApproved := true func (p *LetterOutgoingProcessorImpl) areAllRequiredApprovalsCompleted(approvalsByStep map[int][]entities.LetterOutgoingApproval) bool {
for _, a := range allApprovals { for _, stepApprovals := range approvalsByStep {
if a.IsRequired && a.Status != entities.ApprovalStatusApproved { for _, approval := range stepApprovals {
allRequiredApproved = false if approval.IsRequired && approval.Status != entities.ApprovalStatusApproved {
break return false
}
}
// Update letter status if all required approvals are done
if allRequiredApproved {
if err := p.letterRepo.UpdateStatus(txCtx, letterID, entities.LetterOutgoingStatusApproved); err != nil {
return err
} }
} }
}
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{ activityLog := &entities.LetterOutgoingActivityLog{
LetterID: letterID, LetterID: letterID,
ActionType: entities.LetterOutgoingActionApproved, ActionType: entities.LetterOutgoingActionApproved,
ActorUserID: &userID, ActorUserID: &userID,
TargetID: &approval.ID, TargetID: &approvalID,
} }
if err := p.activityLogRepo.Create(txCtx, activityLog); err != nil { return p.activityLogRepo.Create(ctx, activityLog)
return err
}
return nil
})
} }
func (p *LetterOutgoingProcessorImpl) ProcessRejection(ctx context.Context, letterID uuid.UUID, approval *entities.LetterOutgoingApproval, userID uuid.UUID) error { 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 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 return err
} }
fromStatus := string(entities.LetterOutgoingStatusPendingApproval) fromStatus := string(entities.LetterOutgoingStatusPendingApproval)
toStatus := string(entities.LetterOutgoingStatusDraft) toStatus := string(entities.LetterOutgoingStatusRejected)
activityLog := &entities.LetterOutgoingActivityLog{ activityLog := &entities.LetterOutgoingActivityLog{
LetterID: letterID, LetterID: letterID,
ActionType: entities.LetterOutgoingActionRejected, ActionType: entities.LetterOutgoingActionRejected,

View File

@ -623,6 +623,10 @@ func (p *LetterProcessorImpl) BulkArchiveIncomingLetters(ctx context.Context, le
return p.letterRepo.BulkArchive(ctx, letterIDs) 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 // BulkArchiveIncomingLettersForUser archives letters for a specific user only
func (p *LetterProcessorImpl) BulkArchiveIncomingLettersForUser(ctx context.Context, letterIDs []uuid.UUID, userID uuid.UUID) (int64, error) { func (p *LetterProcessorImpl) BulkArchiveIncomingLettersForUser(ctx context.Context, letterIDs []uuid.UUID, userID uuid.UUID) (int64, error) {
return p.letterRepo.BulkArchiveForUser(ctx, letterIDs, userID) return p.letterRepo.BulkArchiveForUser(ctx, letterIDs, userID)

View File

@ -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) { func (r *LetterOutgoingRepository) BulkArchive(ctx context.Context, letterIDs []uuid.UUID) (int64, error) {
db := DBFromContext(ctx, r.db) db := DBFromContext(ctx, r.db)
now := time.Now()
result := db.WithContext(ctx). result := db.WithContext(ctx).
Model(&entities.LetterOutgoing{}). Model(&entities.LetterOutgoing{}).
Where("id IN ? AND deleted_at IS NULL", letterIDs). 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 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) { func (r *LetterOutgoingRepository) GetWithRelations(ctx context.Context, id uuid.UUID, relations []string) (*entities.LetterOutgoing, error) {
db := DBFromContext(ctx, r.db) db := DBFromContext(ctx, r.db)
query := db.WithContext(ctx).Where("id = ? AND deleted_at IS NULL", id) 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 // Apply is_archived filter
if filter.IsArchived != nil { if filter.IsArchived != nil {
if *filter.IsArchived { query = query.Where("letters_outgoing.is_archived = ?", *filter.IsArchived)
query = query.Where("letters_outgoing.status = 'archived'")
} else {
query = query.Where("letters_outgoing.status != 'archived'")
}
} }
if filter.Status != nil { if filter.Status != nil {

View File

@ -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) { func (r *LetterIncomingRepository) BulkArchive(ctx context.Context, letterIDs []uuid.UUID) (int64, error) {
db := DBFromContext(ctx, r.db) db := DBFromContext(ctx, r.db)
// For incoming letters, we archive the recipients, not the letter itself now := time.Now()
// The letter status remains as is (new, in_progress, or completed)
result := db.WithContext(ctx). result := db.WithContext(ctx).
Model(&entities.LetterIncomingRecipient{}). Model(&entities.LetterIncoming{}).
Where("letter_id IN ?", letterIDs). Where("id IN ? AND deleted_at IS NULL", letterIDs).
Update("is_archived", true) Updates(map[string]interface{}{
"is_archived": true,
"archived_at": now,
})
return result.RowsAffected, result.Error 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 // BulkArchiveForUser archives letters for a specific user only
func (r *LetterIncomingRepository) BulkArchiveForUser(ctx context.Context, letterIDs []uuid.UUID, userID uuid.UUID) (int64, error) { func (r *LetterIncomingRepository) BulkArchiveForUser(ctx context.Context, letterIDs []uuid.UUID, userID uuid.UUID) (int64, error) {
db := DBFromContext(ctx, r.db) 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 != nil {
if *filter.IsArchived { if *filter.IsArchived {
query = query.Where("letter_incoming_recipients.is_archived = ?", true) query = query.Where("letters_incoming.is_archived = ?", true)
} else { } 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)
} }
} }

View File

@ -83,6 +83,7 @@ type LetterHandler interface {
UpdateIncomingLetter(c *gin.Context) UpdateIncomingLetter(c *gin.Context)
DeleteIncomingLetter(c *gin.Context) DeleteIncomingLetter(c *gin.Context)
BulkArchiveIncomingLetters(c *gin.Context) BulkArchiveIncomingLetters(c *gin.Context)
ArchiveIncomingLetter(c *gin.Context)
CreateDispositions(c *gin.Context) CreateDispositions(c *gin.Context)
GetEnhancedDispositionsByLetter(c *gin.Context) GetEnhancedDispositionsByLetter(c *gin.Context)

View File

@ -179,6 +179,7 @@ func (r *Router) addAppRoutes(rg *gin.Engine) {
lettersch.PUT("/incoming/:id/read", r.letterHandler.MarkIncomingLetterAsRead) lettersch.PUT("/incoming/:id/read", r.letterHandler.MarkIncomingLetterAsRead)
lettersch.DELETE("/incoming/:id", r.letterHandler.DeleteIncomingLetter) lettersch.DELETE("/incoming/:id", r.letterHandler.DeleteIncomingLetter)
lettersch.POST("/incoming/archive", r.letterHandler.BulkArchiveIncomingLetters) lettersch.POST("/incoming/archive", r.letterHandler.BulkArchiveIncomingLetters)
lettersch.PUT("/incoming/:id/archive", r.letterHandler.ArchiveIncomingLetter)
lettersch.POST("/outgoing", r.letterOutgoingHandler.CreateOutgoingLetter) lettersch.POST("/outgoing", r.letterOutgoingHandler.CreateOutgoingLetter)
lettersch.GET("/outgoing/search", r.letterOutgoingHandler.SearchOutgoingLetters) lettersch.GET("/outgoing/search", r.letterOutgoingHandler.SearchOutgoingLetters)

View File

@ -750,13 +750,13 @@ func (s *LetterOutgoingServiceImpl) ArchiveOutgoingLetter(ctx context.Context, l
return err return err
} }
// Can only archive sent letters
if letter.Status != entities.LetterOutgoingStatusSent { if letter.Status != entities.LetterOutgoingStatusSent {
return gorm.ErrInvalidData return gorm.ErrInvalidData
} }
fromStatus := string(entities.LetterOutgoingStatusSent) // Use the new archive method instead of changing status
toStatus := string(entities.LetterOutgoingStatusArchived) return s.processor.ArchiveOutgoingLetter(ctx, letterID, userID)
return s.processor.UpdateLetterStatus(ctx, letterID, entities.LetterOutgoingStatusArchived, userID, &fromStatus, &toStatus)
} }
func (s *LetterOutgoingServiceImpl) AddRecipients(ctx context.Context, letterID uuid.UUID, req *contract.AddRecipientsRequest) error { func (s *LetterOutgoingServiceImpl) AddRecipients(ctx context.Context, letterID uuid.UUID, req *contract.AddRecipientsRequest) error {

View File

@ -32,6 +32,7 @@ type LetterProcessor interface {
UpdateIncomingLetter(ctx context.Context, id uuid.UUID, req *contract.UpdateIncomingLetterRequest) (*contract.IncomingLetterResponse, error) UpdateIncomingLetter(ctx context.Context, id uuid.UUID, req *contract.UpdateIncomingLetterRequest) (*contract.IncomingLetterResponse, error)
SoftDeleteIncomingLetter(ctx context.Context, id uuid.UUID) error SoftDeleteIncomingLetter(ctx context.Context, id uuid.UUID) error
BulkArchiveIncomingLetters(ctx context.Context, letterIDs []uuid.UUID) (int64, 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) BulkArchiveIncomingLettersForUser(ctx context.Context, letterIDs []uuid.UUID, userID uuid.UUID) (int64, error)
// Batch loading methods // 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) { func (s *LetterServiceImpl) BulkArchiveIncomingLetters(ctx context.Context, letterIDs []uuid.UUID) (*contract.BulkArchiveLettersResponse, error) {
// Extract user context to archive only for the current user // Archive the letters themselves
appCtx := appcontext.FromGinContext(ctx) archivedCount, err := s.processor.BulkArchiveIncomingLetters(ctx, letterIDs)
userID := appCtx.UserID
// Archive letters only for the current user
archivedCount, err := s.processor.BulkArchiveIncomingLettersForUser(ctx, letterIDs, userID)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -833,6 +830,10 @@ func (s *LetterServiceImpl) BulkArchiveIncomingLetters(ctx context.Context, lett
}, nil }, 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) { func (s *LetterServiceImpl) sendDiscussionMentionNotifications(ctx context.Context, letterID uuid.UUID, senderUserID uuid.UUID, mentions map[string]interface{}, message string) {
// Extract user_ids from mentions // Extract user_ids from mentions
userIDs := s.extractUserIDsFromMentions(mentions) userIDs := s.extractUserIDsFromMentions(mentions)

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;