Merge pull request 'add archive' (#1) from archived into main

Reviewed-on: ESLOGAD/eslogad-backend#1
This commit is contained in:
altru 2025-10-13 00:47:16 +00:00
commit e67b9a357f
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
@ -45,14 +46,13 @@ type LetterOutgoingProcessor interface {
GetOutgoingLetterWithDetails(ctx context.Context, letterID uuid.UUID) (*entities.LetterOutgoing, error) GetOutgoingLetterWithDetails(ctx context.Context, letterID uuid.UUID) (*entities.LetterOutgoing, error)
GetUsersByIDs(ctx context.Context, userIDs []uuid.UUID) ([]entities.User, error) GetUsersByIDs(ctx context.Context, userIDs []uuid.UUID) ([]entities.User, error)
BulkArchiveOutgoingLetters(ctx context.Context, letterIDs []uuid.UUID) (int64, error) BulkArchiveOutgoingLetters(ctx context.Context, letterIDs []uuid.UUID) (int64, error)
// Batch loading methods for efficient querying // Batch loading methods for efficient querying
GetBatchAttachments(ctx context.Context, letterIDs []uuid.UUID) (map[uuid.UUID][]entities.LetterOutgoingAttachment, error) 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) 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) 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,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 { 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 {
now := time.Now() // Step 1: Update the approval record
approval.Status = entities.ApprovalStatusApproved if err := p.updateApprovalRecord(txCtx, approval, userID); err != nil {
approval.ApproverID = &userID
approval.ActedAt = &now
if err := p.approvalRepo.Update(txCtx, approval); err != nil {
return err 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 { if err != nil {
return err return err
} }
approvalsByStep := make(map[int][]entities.LetterOutgoingApproval) // Step 3: Check if current step is completed
for _, a := range allApprovals { if p.isStepCompleted(approvalsByStep[approval.StepOrder]) {
approvalsByStep[a.StepOrder] = append(approvalsByStep[a.StepOrder], a) // Step 4: Activate next step if exists
} if err := p.activateNextStep(txCtx, letterID, approval.StepOrder, approvalsByStep); err != nil {
return err
currentStepCompleted := true
for _, a := range approvalsByStep[approval.StepOrder] {
if a.IsRequired && a.Status != entities.ApprovalStatusApproved {
currentStepCompleted = false
break
} }
} }
// If current step is completed, activate the next step and add approvers as recipients // Step 5: Check if all required approvals are completed
if currentStepCompleted { if p.areAllRequiredApprovalsCompleted(approvalsByStep) {
nextStepOrder := approval.StepOrder + 1 // Step 6: Update letter status to approved
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 {
if err := p.letterRepo.UpdateStatus(txCtx, letterID, entities.LetterOutgoingStatusApproved); err != nil { if err := p.letterRepo.UpdateStatus(txCtx, letterID, entities.LetterOutgoingStatusApproved); err != nil {
return err return err
} }
} }
activityLog := &entities.LetterOutgoingActivityLog{ // Step 7: Log the activity
LetterID: letterID, return p.logApprovalActivity(txCtx, letterID, approval.ID, userID)
ActionType: entities.LetterOutgoingActionApproved, })
ActorUserID: &userID, }
TargetID: &approval.ID,
// 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 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 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 { 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)
@ -104,11 +120,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

@ -753,13 +753,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;