Merge pull request 'add archive' (#1) from archived into main
Reviewed-on: ESLOGAD/eslogad-backend#1
This commit is contained in:
commit
e67b9a357f
@ -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"`
|
||||
|
||||
@ -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"`
|
||||
|
||||
@ -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"})
|
||||
}
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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)
|
||||
@ -104,11 +120,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 {
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -753,13 +753,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 {
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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;
|
||||
@ -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;
|
||||
@ -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;
|
||||
@ -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;
|
||||
Loading…
x
Reference in New Issue
Block a user