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
+173 -84
View File
@@ -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,
+4
View File
@@ -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)