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) 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 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 ProcessRejection(ctx context.Context, letterID uuid.UUID, approval *entities.LetterOutgoingApproval, 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) 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) } 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 } 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, ) *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, } } 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 } } // Collect all recipients from the first step (can be multiple if parallel) 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) } } // If no recipients were created, return without error if len(recipients) == 0 { return nil } // Bulk create all recipients return p.recipientRepo.CreateBulk(ctx, recipients) } // 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 case entities.LetterOutgoingStatusArchived: activityLog.ActionType = entities.LetterOutgoingActionArchived default: activityLog.ActionType = entities.LetterOutgoingActionUpdated } 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 { approvals := make([]entities.LetterOutgoingApproval, len(flow.Steps)) for i, step := range flow.Steps { approvals[i] = entities.LetterOutgoingApproval{ LetterID: letterID, StepID: step.ID, Status: entities.ApprovalStatusPending, } } if err := p.approvalRepo.CreateBulk(txCtx, approvals); err != nil { return err } 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, 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 { return err } if allApproved { 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, } if err := p.activityLogRepo.Create(txCtx, activityLog); err != nil { return err } return nil }) } 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 } if err := p.letterRepo.UpdateStatus(txCtx, letterID, entities.LetterOutgoingStatusDraft); err != nil { return err } fromStatus := string(entities.LetterOutgoingStatusPendingApproval) toStatus := string(entities.LetterOutgoingStatusDraft) activityLog := &entities.LetterOutgoingActivityLog{ LetterID: letterID, ActionType: entities.LetterOutgoingActionRejected, ActorUserID: &userID, TargetID: &approval.ID, FromStatus: &fromStatus, ToStatus: &toStatus, } 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 { 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) { return p.approvalRepo.ListByLetter(ctx, letterID) } 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 }