package processor import ( "context" "time" "eslogad-be/internal/contract" "eslogad-be/internal/entities" "eslogad-be/internal/repository" "github.com/google/uuid" "gorm.io/gorm" ) type LetterOutgoingProcessor interface { CreateOutgoingLetter(ctx context.Context, letter *entities.LetterOutgoing, attachments []entities.LetterOutgoingAttachment, userID, departmentID uuid.UUID) error GetOutgoingLetterByID(ctx context.Context, id uuid.UUID) (*entities.LetterOutgoing, error) ListOutgoingLetters(ctx context.Context, filter repository.ListOutgoingLettersFilter, limit, offset int) ([]entities.LetterOutgoing, int64, error) SearchOutgoingLetters(ctx context.Context, filters map[string]interface{}, page, limit int, sortBy, sortOrder string) ([]entities.LetterOutgoing, int64, error) UpdateOutgoingLetter(ctx context.Context, letter *entities.LetterOutgoing, 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 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) error ProcessRejection(ctx context.Context, letterID uuid.UUID, approval *entities.LetterOutgoingApproval, userID uuid.UUID) error ProcessRevision(ctx context.Context, letterID uuid.UUID, attachment entities.LetterOutgoingAttachment, userID uuid.UUID) error AddRecipients(ctx context.Context, letterID uuid.UUID, recipients []entities.LetterOutgoingRecipient, userID uuid.UUID) error UpdateRecipient(ctx context.Context, recipient *entities.LetterOutgoingRecipient) error RemoveRecipient(ctx context.Context, letterID uuid.UUID, recipientID uuid.UUID, userID uuid.UUID) error AddAttachments(ctx context.Context, letterID uuid.UUID, attachments []entities.LetterOutgoingAttachment, userID uuid.UUID) error RemoveAttachment(ctx context.Context, letterID uuid.UUID, attachmentID uuid.UUID, userID uuid.UUID) error CreateDiscussion(ctx context.Context, discussion *entities.LetterOutgoingDiscussion, attachments []entities.LetterOutgoingDiscussionAttachment, userID uuid.UUID) error GetDiscussionByID(ctx context.Context, id uuid.UUID) (*entities.LetterOutgoingDiscussion, error) UpdateDiscussion(ctx context.Context, discussion *entities.LetterOutgoingDiscussion) error DeleteDiscussion(ctx context.Context, id uuid.UUID) error GetApprovalsByLetter(ctx context.Context, letterID uuid.UUID) ([]entities.LetterOutgoingApproval, error) GetAllApprovalsByLetter(ctx context.Context, letterID uuid.UUID) ([]entities.LetterOutgoingApproval, error) GetApprovalsByLetterAndRevision(ctx context.Context, letterID uuid.UUID, revisionNumber int) ([]entities.LetterOutgoingApproval, error) GetApprovalFlow(ctx context.Context, flowID uuid.UUID) (*entities.ApprovalFlow, error) // GetOutgoingLetterWithDetails fetches letter with all related data 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 { db *gorm.DB letterRepo *repository.LetterOutgoingRepository attachmentRepo *repository.LetterOutgoingAttachmentRepository recipientRepo *repository.LetterOutgoingRecipientRepository discussionRepo *repository.LetterOutgoingDiscussionRepository discussionAttachmentRepo *repository.LetterOutgoingDiscussionAttachmentRepository activityLogRepo *repository.LetterOutgoingActivityLogRepository approvalFlowRepo *repository.ApprovalFlowRepository approvalRepo *repository.LetterOutgoingApprovalRepository numberGenerator *LetterNumberGeneratorImpl txManager *repository.TxManager priorityRepo *repository.PriorityRepository institutionRepo *repository.InstitutionRepository } func NewLetterOutgoingProcessor( db *gorm.DB, letterRepo *repository.LetterOutgoingRepository, attachmentRepo *repository.LetterOutgoingAttachmentRepository, recipientRepo *repository.LetterOutgoingRecipientRepository, discussionRepo *repository.LetterOutgoingDiscussionRepository, discussionAttachmentRepo *repository.LetterOutgoingDiscussionAttachmentRepository, activityLogRepo *repository.LetterOutgoingActivityLogRepository, approvalFlowRepo *repository.ApprovalFlowRepository, approvalRepo *repository.LetterOutgoingApprovalRepository, numberGenerator *LetterNumberGeneratorImpl, txManager *repository.TxManager, priorityRepo *repository.PriorityRepository, institutionRepo *repository.InstitutionRepository, ) *LetterOutgoingProcessorImpl { return &LetterOutgoingProcessorImpl{ db: db, letterRepo: letterRepo, attachmentRepo: attachmentRepo, recipientRepo: recipientRepo, discussionRepo: discussionRepo, discussionAttachmentRepo: discussionAttachmentRepo, activityLogRepo: activityLogRepo, approvalFlowRepo: approvalFlowRepo, approvalRepo: approvalRepo, numberGenerator: numberGenerator, txManager: txManager, priorityRepo: priorityRepo, institutionRepo: institutionRepo, } } func (p *LetterOutgoingProcessorImpl) CreateOutgoingLetter(ctx context.Context, letter *entities.LetterOutgoing, attachments []entities.LetterOutgoingAttachment, userID, departmentID uuid.UUID) error { return p.txManager.WithTransaction(ctx, func(txCtx context.Context) error { // Step 1: Assign approval flow from department if not provided if err := p.assignApprovalFlowFromDepartment(txCtx, letter, departmentID); err != nil { // Log error but continue - approval flow is optional } // Step 2: Set status based on approval flow if letter.ApprovalFlowID != nil { letter.Status = entities.LetterOutgoingStatusPendingApproval } else { letter.Status = entities.LetterOutgoingStatusApproved } // Step 3: Generate and assign letter number if err := p.assignLetterNumber(txCtx, letter); err != nil { return err } // Step 4: Create the letter if err := p.letterRepo.Create(txCtx, letter); err != nil { return err } // Step 5: Create recipients from approval flow if err := p.createRecipientsFromApprovalFlow(txCtx, letter); err != nil { return err } // Step 6: Create attachments if err := p.createAttachments(txCtx, letter.ID, attachments); err != nil { return err } // Step 7: Log the activity return p.logActivity(txCtx, letter.ID, entities.LetterOutgoingActionCreated, userID) }) } func (p *LetterOutgoingProcessorImpl) assignApprovalFlowFromDepartment(ctx context.Context, letter *entities.LetterOutgoing, departmentID uuid.UUID) error { if letter.ApprovalFlowID != nil || departmentID == uuid.Nil { return nil } flow, err := p.approvalFlowRepo.GetByDepartment(ctx, departmentID) if err != nil { return err } if flow != nil { letter.ApprovalFlowID = &flow.ID } return nil } func (p *LetterOutgoingProcessorImpl) assignLetterNumber(ctx context.Context, letter *entities.LetterOutgoing) error { letterNumber, err := p.numberGenerator.GenerateNumber( ctx, contract.SettingOutgoingLetterPrefix, contract.SettingOutgoingLetterSequence, "ESLO", ) if err != nil { return err } letter.LetterNumber = letterNumber return nil } func (p *LetterOutgoingProcessorImpl) createRecipientsFromApprovalFlow(ctx context.Context, letter *entities.LetterOutgoing) error { if letter.ApprovalFlowID == nil { return nil } flow, err := p.approvalFlowRepo.Get(ctx, *letter.ApprovalFlowID) if err != nil || flow == nil || len(flow.Steps) == 0 { return err } // Find the minimum step order (first step) minStepOrder := flow.Steps[0].StepOrder for _, step := range flow.Steps { if step.StepOrder < minStepOrder { minStepOrder = step.StepOrder } } // Create all approval steps in letter_outgoing_approvals var approvals []entities.LetterOutgoingApproval for _, step := range flow.Steps { approval := entities.LetterOutgoingApproval{ LetterID: letter.ID, StepID: step.ID, StepOrder: step.StepOrder, ParallelGroup: step.ParallelGroup, IsRequired: step.Required, ApproverID: step.ApproverUserID, } // Set status based on step order if step.StepOrder == minStepOrder { // First step(s) are set to pending approval.Status = entities.ApprovalStatusPending } else { // All other steps are set to not_started approval.Status = entities.ApprovalStatusNotStarted } approvals = append(approvals, approval) } // Bulk create all approvals if len(approvals) > 0 { if err := p.approvalRepo.CreateBulk(ctx, approvals); err != nil { return err } } // Also create recipients from the first step (for backward compatibility) var recipients []entities.LetterOutgoingRecipient for i, step := range flow.Steps { // Only process steps with the minimum step order (first step) if step.StepOrder != minStepOrder { continue } recipient := p.createRecipientFromApprovalStep(step, letter.ID) if recipient != nil { // Mark the first recipient as primary if i == 0 { recipient.IsPrimary = true } else { recipient.IsPrimary = false } recipients = append(recipients, *recipient) } } // Bulk create all recipients if any if len(recipients) > 0 { return p.recipientRepo.CreateBulk(ctx, recipients) } return nil } // createRecipientFromApprovalStep creates a recipient from an approval flow step func (p *LetterOutgoingProcessorImpl) createRecipientFromApprovalStep(step entities.ApprovalFlowStep, letterID uuid.UUID) *entities.LetterOutgoingRecipient { recipient := &entities.LetterOutgoingRecipient{ LetterID: letterID, Status: "pending", IsArchived: false, UserID: &step.ApproverUser.ID, } return recipient } // createAttachments creates letter attachments func (p *LetterOutgoingProcessorImpl) createAttachments(ctx context.Context, letterID uuid.UUID, attachments []entities.LetterOutgoingAttachment) error { if len(attachments) == 0 { return nil } // Update letter IDs for all attachments for i := range attachments { attachments[i].LetterID = letterID } return p.attachmentRepo.CreateBulk(ctx, attachments) } // logActivity logs an activity for the letter func (p *LetterOutgoingProcessorImpl) logActivity(ctx context.Context, letterID uuid.UUID, actionType string, userID uuid.UUID) error { activityLog := &entities.LetterOutgoingActivityLog{ LetterID: letterID, ActionType: actionType, ActorUserID: &userID, } return p.activityLogRepo.Create(ctx, activityLog) } func (p *LetterOutgoingProcessorImpl) GetOutgoingLetterByID(ctx context.Context, id uuid.UUID) (*entities.LetterOutgoing, error) { return p.letterRepo.Get(ctx, id) } func (p *LetterOutgoingProcessorImpl) ListOutgoingLetters(ctx context.Context, filter repository.ListOutgoingLettersFilter, limit, offset int) ([]entities.LetterOutgoing, int64, error) { return p.letterRepo.List(ctx, filter, limit, offset) } func (p *LetterOutgoingProcessorImpl) UpdateOutgoingLetter(ctx context.Context, letter *entities.LetterOutgoing, userID uuid.UUID) error { return p.txManager.WithTransaction(ctx, func(txCtx context.Context) error { if err := p.letterRepo.Update(txCtx, letter); err != nil { return err } activityLog := &entities.LetterOutgoingActivityLog{ LetterID: letter.ID, ActionType: entities.LetterOutgoingActionUpdated, ActorUserID: &userID, } if err := p.activityLogRepo.Create(txCtx, activityLog); err != nil { return err } return nil }) } func (p *LetterOutgoingProcessorImpl) DeleteOutgoingLetter(ctx context.Context, id uuid.UUID, userID uuid.UUID) error { return p.txManager.WithTransaction(ctx, func(txCtx context.Context) error { if err := p.letterRepo.SoftDelete(txCtx, id); err != nil { return err } letter, _ := p.letterRepo.Get(txCtx, id) activityLog := &entities.LetterOutgoingActivityLog{ LetterID: letter.ID, ActionType: entities.LetterOutgoingActionDeleted, ActorUserID: &userID, } if err := p.activityLogRepo.Create(txCtx, activityLog); err != nil { return err } return nil }) } func (p *LetterOutgoingProcessorImpl) UpdateLetterStatus(ctx context.Context, letterID uuid.UUID, status entities.LetterOutgoingStatus, userID uuid.UUID, fromStatus, toStatus *string) error { return p.txManager.WithTransaction(ctx, func(txCtx context.Context) error { if err := p.letterRepo.UpdateStatus(txCtx, letterID, status); err != nil { return err } activityLog := &entities.LetterOutgoingActivityLog{ LetterID: letterID, ActorUserID: &userID, FromStatus: fromStatus, ToStatus: toStatus, } switch status { case entities.LetterOutgoingStatusPendingApproval: activityLog.ActionType = entities.LetterOutgoingActionSubmittedApproval case entities.LetterOutgoingStatusApproved: activityLog.ActionType = entities.LetterOutgoingActionApproved case entities.LetterOutgoingStatusSent: activityLog.ActionType = entities.LetterOutgoingActionSent default: activityLog.ActionType = entities.LetterOutgoingActionUpdated } if err := p.activityLogRepo.Create(txCtx, activityLog); err != nil { return err } return nil }) } 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 { return err } return p.txManager.WithTransaction(ctx, func(txCtx context.Context) error { // Get the letter to get the current revision number letter, err := p.letterRepo.Get(txCtx, letterID) if err != nil { return err } // Find the minimum step order (first step) minStepOrder := flow.Steps[0].StepOrder for _, step := range flow.Steps { if step.StepOrder < minStepOrder { minStepOrder = step.StepOrder } } approvals := make([]entities.LetterOutgoingApproval, len(flow.Steps)) for i, step := range flow.Steps { approvals[i] = entities.LetterOutgoingApproval{ LetterID: letterID, StepID: step.ID, RevisionNumber: letter.RevisionNumber, StepOrder: step.StepOrder, ParallelGroup: step.ParallelGroup, IsRequired: step.Required, ApproverID: step.ApproverUserID, Status: entities.ApprovalStatusPending, } // Set status based on step order if step.StepOrder == minStepOrder { approvals[i].Status = entities.ApprovalStatusPending } else { approvals[i].Status = entities.ApprovalStatusNotStarted } } if err := p.approvalRepo.CreateBulk(txCtx, approvals); err != nil { return err } // Add first step approvers as recipients existingRecipients, err := p.recipientRepo.ListByLetter(txCtx, letterID) if err != nil { return err } // Create a map of existing user IDs for quick lookup existingUserIDs := make(map[uuid.UUID]bool) for _, recipient := range existingRecipients { if recipient.UserID != nil { existingUserIDs[*recipient.UserID] = true } } // Add approvers from the first step as recipients for _, approval := range approvals { if approval.StepOrder == minStepOrder && approval.ApproverID != nil { if !existingUserIDs[*approval.ApproverID] { newRecipient := entities.LetterOutgoingRecipient{ LetterID: letterID, UserID: approval.ApproverID, IsPrimary: false, Status: "unread", IsArchived: false, } if err := p.recipientRepo.Create(txCtx, &newRecipient); err != nil { return err } existingUserIDs[*approval.ApproverID] = true } } } if err := p.letterRepo.UpdateStatus(txCtx, letterID, entities.LetterOutgoingStatusPendingApproval); err != nil { return err } fromStatus := string(entities.LetterOutgoingStatusDraft) toStatus := string(entities.LetterOutgoingStatusPendingApproval) activityLog := &entities.LetterOutgoingActivityLog{ LetterID: letterID, ActionType: entities.LetterOutgoingActionSubmittedApproval, ActorUserID: &userID, FromStatus: &fromStatus, ToStatus: &toStatus, } if err := p.activityLogRepo.Create(txCtx, activityLog); err != nil { return err } return nil }) } func (p *LetterOutgoingProcessorImpl) ProcessApproval(ctx context.Context, letterID uuid.UUID, approval *entities.LetterOutgoingApproval, userID uuid.UUID) 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 FOR THE SAME REVISION and organize by step approvalsByStep, err := p.getApprovalsByStepForRevision(txCtx, letterID, approval.RevisionNumber) 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.activateNextStepForRevision(txCtx, letterID, approval.StepOrder, approval.RevisionNumber, approvalsByStep); err != nil { return err } } // Step 5: Check if all required approvals are completed FOR THIS REVISION 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() 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 } // getApprovalsByStepForRevision fetches approvals for a specific revision and organizes them by step order func (p *LetterOutgoingProcessorImpl) getApprovalsByStepForRevision(ctx context.Context, letterID uuid.UUID, revisionNumber int) (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 { // Only include approvals from the same revision if approval.RevisionNumber == revisionNumber { 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 } } 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 } // activateNextStepForRevision activates the next approval step for a specific revision func (p *LetterOutgoingProcessorImpl) activateNextStepForRevision(ctx context.Context, letterID uuid.UUID, currentStepOrder int, revisionNumber 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 (already filtered by revision in approvalsByStep) for _, nextApproval := range nextStepApprovals { // Only process if it's the same revision if nextApproval.RevisionNumber == revisionNumber { // 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 { return p.txManager.WithTransaction(ctx, func(txCtx context.Context) error { now := time.Now() approval.Status = entities.ApprovalStatusRejected approval.ApproverID = &userID approval.ActedAt = &now if err := p.approvalRepo.Update(txCtx, approval); err != nil { return err } // Mark all other pending approvals in the same revision as rejected allApprovals, err := p.approvalRepo.ListByLetter(txCtx, letterID) if err != nil { return err } for i := range allApprovals { // Only update other pending approvals from the same revision if allApprovals[i].RevisionNumber == approval.RevisionNumber && allApprovals[i].ID != approval.ID && allApprovals[i].Status == entities.ApprovalStatusPending { allApprovals[i].Status = entities.ApprovalStatusRejected if err := p.approvalRepo.Update(txCtx, &allApprovals[i]); err != nil { return err } } } if err := p.letterRepo.UpdateStatus(txCtx, letterID, entities.LetterOutgoingStatusRejected); err != nil { return err } fromStatus := string(entities.LetterOutgoingStatusPendingApproval) toStatus := string(entities.LetterOutgoingStatusRejected) // Include rejection remarks in activity log context var context entities.JSONB if approval.Remarks != nil && *approval.Remarks != "" { context = entities.JSONB{"remarks": *approval.Remarks, "revision_number": approval.RevisionNumber} } else { context = entities.JSONB{"revision_number": approval.RevisionNumber} } activityLog := &entities.LetterOutgoingActivityLog{ LetterID: letterID, ActionType: entities.LetterOutgoingActionRejected, ActorUserID: &userID, TargetID: &approval.ID, FromStatus: &fromStatus, ToStatus: &toStatus, Context: context, } if err := p.activityLogRepo.Create(txCtx, activityLog); err != nil { return err } return nil }) } func (p *LetterOutgoingProcessorImpl) ProcessRevision(ctx context.Context, letterID uuid.UUID, attachment entities.LetterOutgoingAttachment, userID uuid.UUID) error { return p.txManager.WithTransaction(ctx, func(txCtx context.Context) error { // Get the current letter letter, err := p.letterRepo.Get(txCtx, letterID) if err != nil { return err } // Increment revision number letter.RevisionNumber++ // Set revision number on the new attachment attachment.RevisionNumber = letter.RevisionNumber // Add the new attachment if err := p.attachmentRepo.Create(txCtx, &attachment); err != nil { return err } // Update letter with new revision number if err := p.letterRepo.Update(txCtx, letter); err != nil { return err } // Update status to pending approval (ready for re-submission) if err := p.letterRepo.UpdateStatus(txCtx, letterID, entities.LetterOutgoingStatusPendingApproval); err != nil { return err } // Get existing approvals for the current revision approvals, err := p.approvalRepo.ListByLetter(txCtx, letterID) if err != nil { return err } // Create new approval records for the new revision for _, approval := range approvals { // Create a new approval for the new revision newApproval := entities.LetterOutgoingApproval{ LetterID: approval.LetterID, StepID: approval.StepID, RevisionNumber: letter.RevisionNumber, StepOrder: approval.StepOrder, ParallelGroup: approval.ParallelGroup, IsRequired: approval.IsRequired, Status: entities.ApprovalStatusPending, ApproverID: approval.ApproverID, } if err := p.approvalRepo.Create(txCtx, &newApproval); err != nil { return err } } // Log activity fromStatus := string(entities.LetterOutgoingStatusRejected) toStatus := string(entities.LetterOutgoingStatusPendingApproval) activityLog := &entities.LetterOutgoingActivityLog{ LetterID: letterID, ActionType: entities.LetterOutgoingActionRevised, ActorUserID: &userID, TargetID: &attachment.ID, FromStatus: &fromStatus, ToStatus: &toStatus, Context: entities.JSONB{"attachment": attachment.FileName, "revision_number": letter.RevisionNumber}, } if err := p.activityLogRepo.Create(txCtx, activityLog); err != nil { return err } return nil }) } func (p *LetterOutgoingProcessorImpl) AddRecipients(ctx context.Context, letterID uuid.UUID, recipients []entities.LetterOutgoingRecipient, userID uuid.UUID) error { return p.txManager.WithTransaction(ctx, func(txCtx context.Context) error { if err := p.recipientRepo.CreateBulk(txCtx, recipients); err != nil { return err } activityLog := &entities.LetterOutgoingActivityLog{ LetterID: letterID, ActionType: entities.LetterOutgoingActionRecipientAdded, ActorUserID: &userID, } if err := p.activityLogRepo.Create(txCtx, activityLog); err != nil { return err } return nil }) } func (p *LetterOutgoingProcessorImpl) UpdateRecipient(ctx context.Context, recipient *entities.LetterOutgoingRecipient) error { return p.recipientRepo.Update(ctx, recipient) } func (p *LetterOutgoingProcessorImpl) RemoveRecipient(ctx context.Context, letterID uuid.UUID, recipientID uuid.UUID, userID uuid.UUID) error { return p.txManager.WithTransaction(ctx, func(txCtx context.Context) error { if err := p.recipientRepo.Delete(txCtx, recipientID); err != nil { return err } activityLog := &entities.LetterOutgoingActivityLog{ LetterID: letterID, ActionType: entities.LetterOutgoingActionRecipientRemoved, ActorUserID: &userID, TargetID: &recipientID, } if err := p.activityLogRepo.Create(txCtx, activityLog); err != nil { return err } return nil }) } func (p *LetterOutgoingProcessorImpl) AddAttachments(ctx context.Context, letterID uuid.UUID, attachments []entities.LetterOutgoingAttachment, userID uuid.UUID) error { return p.txManager.WithTransaction(ctx, func(txCtx context.Context) error { // Get the letter to get the current revision number letter, err := p.letterRepo.Get(txCtx, letterID) if err != nil { return err } // Set revision number on all attachments for i := range attachments { attachments[i].RevisionNumber = letter.RevisionNumber } if err := p.attachmentRepo.CreateBulk(txCtx, attachments); err != nil { return err } activityLog := &entities.LetterOutgoingActivityLog{ LetterID: letterID, ActionType: entities.LetterOutgoingActionAttachmentAdded, ActorUserID: &userID, } if err := p.activityLogRepo.Create(txCtx, activityLog); err != nil { return err } return nil }) } func (p *LetterOutgoingProcessorImpl) RemoveAttachment(ctx context.Context, letterID uuid.UUID, attachmentID uuid.UUID, userID uuid.UUID) error { return p.txManager.WithTransaction(ctx, func(txCtx context.Context) error { if err := p.attachmentRepo.Delete(txCtx, attachmentID); err != nil { return err } activityLog := &entities.LetterOutgoingActivityLog{ LetterID: letterID, ActionType: entities.LetterOutgoingActionAttachmentRemoved, ActorUserID: &userID, TargetID: &attachmentID, } if err := p.activityLogRepo.Create(txCtx, activityLog); err != nil { return err } return nil }) } func (p *LetterOutgoingProcessorImpl) CreateDiscussion(ctx context.Context, discussion *entities.LetterOutgoingDiscussion, attachments []entities.LetterOutgoingDiscussionAttachment, userID uuid.UUID) error { return p.txManager.WithTransaction(ctx, func(txCtx context.Context) error { if err := p.discussionRepo.Create(txCtx, discussion); err != nil { return err } if len(attachments) > 0 { if err := p.discussionAttachmentRepo.CreateBulk(txCtx, attachments); err != nil { return err } } activityLog := &entities.LetterOutgoingActivityLog{ LetterID: discussion.LetterID, ActionType: entities.LetterOutgoingActionDiscussionAdded, ActorUserID: &userID, TargetID: &discussion.ID, } if err := p.activityLogRepo.Create(txCtx, activityLog); err != nil { return err } return nil }) } func (p *LetterOutgoingProcessorImpl) GetDiscussionByID(ctx context.Context, id uuid.UUID) (*entities.LetterOutgoingDiscussion, error) { return p.discussionRepo.Get(ctx, id) } func (p *LetterOutgoingProcessorImpl) UpdateDiscussion(ctx context.Context, discussion *entities.LetterOutgoingDiscussion) error { return p.discussionRepo.Update(ctx, discussion) } func (p *LetterOutgoingProcessorImpl) DeleteDiscussion(ctx context.Context, id uuid.UUID) error { return p.discussionRepo.Delete(ctx, id) } func (p *LetterOutgoingProcessorImpl) GetApprovalsByLetter(ctx context.Context, letterID uuid.UUID) ([]entities.LetterOutgoingApproval, error) { // Get the letter first to know the current revision letter, err := p.letterRepo.Get(ctx, letterID) if err != nil { return nil, err } return p.GetApprovalsByLetterAndRevision(ctx, letterID, letter.RevisionNumber) } func (p *LetterOutgoingProcessorImpl) GetAllApprovalsByLetter(ctx context.Context, letterID uuid.UUID) ([]entities.LetterOutgoingApproval, error) { approvals, err := p.approvalRepo.ListByLetter(ctx, letterID) if err != nil { return nil, err } return approvals, nil } func (p *LetterOutgoingProcessorImpl) GetApprovalsByLetterAndRevision(ctx context.Context, letterID uuid.UUID, revisionNumber int) ([]entities.LetterOutgoingApproval, error) { // Get all approvals for this letter approvals, err := p.approvalRepo.ListByLetter(ctx, letterID) if err != nil { return nil, err } // Filter to only return approvals for the specified revision var currentRevisionApprovals []entities.LetterOutgoingApproval for _, approval := range approvals { if approval.RevisionNumber == revisionNumber { currentRevisionApprovals = append(currentRevisionApprovals, approval) } } return currentRevisionApprovals, nil } func (p *LetterOutgoingProcessorImpl) GetApprovalFlow(ctx context.Context, flowID uuid.UUID) (*entities.ApprovalFlow, error) { return p.approvalFlowRepo.Get(ctx, flowID) } // GetOutgoingLetterWithDetails fetches letter with all related data including approvals, discussions, and users func (p *LetterOutgoingProcessorImpl) GetOutgoingLetterWithDetails(ctx context.Context, letterID uuid.UUID) (*entities.LetterOutgoing, error) { letter, err := p.letterRepo.GetWithRelations(ctx, letterID, []string{ "Priority", "ReceiverInstitution", "Creator", "Creator.Profile", "ApprovalFlow", "ApprovalFlow.Steps", "ApprovalFlow.Steps.ApproverRole", "ApprovalFlow.Steps.ApproverUser", "ApprovalFlow.Steps.ApproverUser.Profile", "Recipients", "Recipients.User", "Recipients.User.Profile", "Recipients.Department", "Attachments", "Approvals", "Approvals.Step", "Approvals.Step.ApproverRole", "Approvals.Step.ApproverUser", "Approvals.Step.ApproverUser.Profile", "Approvals.Approver", "Approvals.Approver.Profile", "Discussions", "Discussions.User", "Discussions.User.Profile", "Discussions.Attachments", "ActivityLogs", }) if err != nil { return nil, err } return letter, nil } // GetUsersByIDs fetches users by their IDs func (p *LetterOutgoingProcessorImpl) GetUsersByIDs(ctx context.Context, userIDs []uuid.UUID) ([]entities.User, error) { if len(userIDs) == 0 { return []entities.User{}, nil } var users []entities.User err := p.db.WithContext(ctx). Preload("Profile"). Where("id IN ?", userIDs). Find(&users).Error if err != nil { return nil, err } return users, nil } func (p *LetterOutgoingProcessorImpl) SearchOutgoingLetters(ctx context.Context, filters map[string]interface{}, page, limit int, sortBy, sortOrder string) ([]entities.LetterOutgoing, int64, error) { offset := (page - 1) * limit return p.letterRepo.Search(ctx, filters, limit, offset, sortBy, sortOrder) } func (p *LetterOutgoingProcessorImpl) BulkArchiveOutgoingLetters(ctx context.Context, letterIDs []uuid.UUID) (int64, error) { return p.letterRepo.BulkArchive(ctx, letterIDs) } // GetBatchAttachments fetches attachments for multiple letters in a single query func (p *LetterOutgoingProcessorImpl) GetBatchAttachments(ctx context.Context, letterIDs []uuid.UUID) (map[uuid.UUID][]entities.LetterOutgoingAttachment, error) { if p.attachmentRepo == nil || len(letterIDs) == 0 { return make(map[uuid.UUID][]entities.LetterOutgoingAttachment), nil } return p.attachmentRepo.ListByLetterIDs(ctx, letterIDs) } // GetBatchRecipients fetches recipients for multiple letters in a single query func (p *LetterOutgoingProcessorImpl) GetBatchRecipients(ctx context.Context, letterIDs []uuid.UUID) (map[uuid.UUID][]entities.LetterOutgoingRecipient, error) { if p.recipientRepo == nil || len(letterIDs) == 0 { return make(map[uuid.UUID][]entities.LetterOutgoingRecipient), nil } return p.recipientRepo.ListByLetterIDs(ctx, letterIDs) } func (p *LetterOutgoingProcessorImpl) GetBatchOutgoingRecipientsByUser(ctx context.Context, letterIDs []uuid.UUID, userID uuid.UUID) (map[uuid.UUID]*entities.LetterOutgoingRecipient, error) { if p.recipientRepo == nil || len(letterIDs) == 0 { return make(map[uuid.UUID]*entities.LetterOutgoingRecipient), nil } return p.recipientRepo.GetByLetterIDsAndUser(ctx, letterIDs, userID) } // GetBatchPriorities fetches priorities by IDs in a single query func (p *LetterOutgoingProcessorImpl) GetBatchPriorities(ctx context.Context, priorityIDs []uuid.UUID) (map[uuid.UUID]*entities.Priority, error) { if p.priorityRepo == nil || len(priorityIDs) == 0 { return make(map[uuid.UUID]*entities.Priority), nil } return p.priorityRepo.GetByIDs(ctx, priorityIDs) } // GetBatchInstitutions fetches institutions by IDs in a single query func (p *LetterOutgoingProcessorImpl) GetBatchInstitutions(ctx context.Context, institutionIDs []uuid.UUID) (map[uuid.UUID]*entities.Institution, error) { if p.institutionRepo == nil || len(institutionIDs) == 0 { return make(map[uuid.UUID]*entities.Institution), nil } return p.institutionRepo.GetByIDs(ctx, institutionIDs) }