package service import ( "context" "fmt" "log" "time" "eslogad-be/internal/appcontext" "eslogad-be/internal/constant" "eslogad-be/internal/contract" "eslogad-be/internal/entities" "eslogad-be/internal/processor" "eslogad-be/internal/repository" "eslogad-be/internal/transformer" "github.com/google/uuid" ) const ( DefaultIncomingLetterID = "ESLI" ) type LetterProcessor interface { CreateIncomingLetter(ctx context.Context, req *contract.CreateIncomingLetterRequest) (*contract.IncomingLetterResponse, error) GetIncomingLetterByID(ctx context.Context, id uuid.UUID) (*contract.IncomingLetterResponse, error) ListIncomingLetters(ctx context.Context, filter repository.ListIncomingLettersFilter, page, limit int) ([]entities.LetterIncoming, int64, error) SearchIncomingLetters(ctx context.Context, filters map[string]interface{}, page, limit int, sortBy, sortOrder string) ([]entities.LetterIncoming, int64, error) GetLetterUnreadCounts(ctx context.Context) (*contract.LetterUnreadCountResponse, error) MarkIncomingLetterAsRead(ctx context.Context, letterID uuid.UUID) (*contract.MarkLetterReadResponse, error) MarkOutgoingLetterAsRead(ctx context.Context, letterID uuid.UUID) (*contract.MarkLetterReadResponse, error) 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 GetBatchAttachments(ctx context.Context, letterIDs []uuid.UUID) (map[uuid.UUID][]entities.LetterIncomingAttachment, error) GetBatchDispositions(ctx context.Context, letterIDs []uuid.UUID) (map[uuid.UUID][]entities.LetterIncomingDisposition, 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) GetBatchRecipientsByUser(ctx context.Context, letterIDs []uuid.UUID, userID uuid.UUID) (map[uuid.UUID]*entities.LetterIncomingRecipient, error) CountUnreadByUser(ctx context.Context, userID uuid.UUID) (int, error) CreateDispositions(ctx context.Context, req *contract.CreateLetterDispositionRequest) (*contract.ListDispositionsResponse, error) GetEnhancedDispositionsByLetter(ctx context.Context, letterID uuid.UUID) (*contract.ListEnhancedDispositionsResponse, error) CreateDiscussion(ctx context.Context, letterID uuid.UUID, req *contract.CreateLetterDiscussionRequest) (*contract.LetterDiscussionResponse, error) UpdateDiscussion(ctx context.Context, letterID uuid.UUID, discussionID uuid.UUID, req *contract.UpdateLetterDiscussionRequest) (*contract.LetterDiscussionResponse, string, error) GetDepartmentDispositionStatus(ctx context.Context, req *contract.GetDepartmentDispositionStatusRequest) (*contract.ListDepartmentDispositionStatusResponse, error) UpdateDispositionStatus(ctx context.Context, req *contract.UpdateDispositionStatusRequest) (*contract.DepartmentDispositionStatusResponse, error) GetLetterCTA(ctx context.Context, letterID uuid.UUID, departmentID uuid.UUID) (*contract.LetterCTAResponse, error) } type LetterServiceImpl struct { processor LetterProcessor txManager *repository.TxManager numberGenerator NumberGenerator recipientProcessor RecipientProcessor activityLogger ActivityLogger letterDispositionProcessor LetterDispositionProcessor notificationProcessor processor.NotificationProcessor activityProcessor ActivityLogger } type NumberGenerator interface { GenerateNumber(ctx context.Context, prefixKey, sequenceKey, defaultPrefix string) (string, error) } type RecipientProcessor interface { CreateDefaultRecipients(ctx context.Context, letterID uuid.UUID) ([]entities.LetterIncomingRecipient, error) CreateRecipients(ctx context.Context, letterID uuid.UUID, departmentIDs []uuid.UUID) ([]entities.LetterIncomingRecipient, error) CreateSingleRecipient(ctx context.Context, recipient *entities.LetterIncomingRecipient) error } type ActivityLogger interface { LogLetterCreated(ctx context.Context, letterID uuid.UUID, userID uuid.UUID, letterNumber string) error LogAttachmentUploaded(ctx context.Context, letterID uuid.UUID, userID uuid.UUID, fileName string, fileType string) error LogLetterDispositionStatusUpdate(ctx context.Context, letterID uuid.UUID, userID uuid.UUID, status string) error } type LetterDispositionProcessor interface { CreateDispositions(ctx context.Context, req *contract.CreateLetterDispositionRequest) (*contract.ListDispositionsResponse, error) } func NewLetterService( processor LetterProcessor, txManager *repository.TxManager, numberGenerator NumberGenerator, recipientProcessor RecipientProcessor, activityLogger ActivityLogger, letterDispositionProcessor LetterDispositionProcessor, notificationProcessor processor.NotificationProcessor, activityProc ActivityLogger, ) *LetterServiceImpl { return &LetterServiceImpl{ processor: processor, txManager: txManager, numberGenerator: numberGenerator, recipientProcessor: recipientProcessor, activityLogger: activityLogger, letterDispositionProcessor: letterDispositionProcessor, notificationProcessor: notificationProcessor, activityProcessor: activityProc, } } func (s *LetterServiceImpl) CreateIncomingLetter(ctx context.Context, req *contract.CreateIncomingLetterRequest) (*contract.IncomingLetterResponse, error) { var result *contract.IncomingLetterResponse var recipients []entities.LetterIncomingRecipient err := s.txManager.WithTransaction(ctx, func(txCtx context.Context) error { letterNumber, err := s.generateLetterNumber(txCtx) if err != nil { return err } req.LetterNumber = letterNumber result, err = s.processor.CreateIncomingLetter(txCtx, req) if err != nil { return err } recipients, err = s.createDefaultRecipients(txCtx, result.ID) if err != nil { return err } if err := s.createDispositionsForRecipients(txCtx, result.ID, recipients); err != nil { return err } s.logLetterCreation(txCtx, result.ID, letterNumber) return nil }) if err != nil { return nil, err } if s.notificationProcessor != nil && len(recipients) > 0 { go s.sendLetterNotifications(context.Background(), result, recipients) } return result, nil } func (s *LetterServiceImpl) generateLetterNumber(ctx context.Context) (string, error) { return s.numberGenerator.GenerateNumber( ctx, contract.SettingIncomingLetterPrefix, contract.SettingIncomingLetterSequence, DefaultIncomingLetterID, ) } func (s *LetterServiceImpl) createDefaultRecipients(ctx context.Context, letterID uuid.UUID) ([]entities.LetterIncomingRecipient, error) { return s.recipientProcessor.CreateDefaultRecipients(ctx, letterID) } func (s *LetterServiceImpl) createDispositionsForRecipients(ctx context.Context, letterID uuid.UUID, recipients []entities.LetterIncomingRecipient) error { if len(recipients) == 0 || s.letterDispositionProcessor == nil { return nil } departmentIDs := s.extractUniqueDepartmentIDs(recipients) if len(departmentIDs) == 0 { return nil } systemDeptID := constant.SystemDepartmentID systemUserID := constant.SystemUserID dispositionReq := &contract.CreateLetterDispositionRequest{ FromDepartment: systemDeptID, LetterID: letterID, ToDepartmentIDs: departmentIDs, Notes: nil, CreatedBy: systemUserID, } _, err := s.letterDispositionProcessor.CreateDispositions(ctx, dispositionReq) return err } func (s *LetterServiceImpl) extractUniqueDepartmentIDs(recipients []entities.LetterIncomingRecipient) []uuid.UUID { deptMap := make(map[uuid.UUID]bool) var departmentIDs []uuid.UUID for _, recipient := range recipients { if recipient.RecipientDepartmentID != nil && !deptMap[*recipient.RecipientDepartmentID] { deptMap[*recipient.RecipientDepartmentID] = true departmentIDs = append(departmentIDs, *recipient.RecipientDepartmentID) } } return departmentIDs } func (s *LetterServiceImpl) logLetterCreation(ctx context.Context, letterID uuid.UUID, letterNumber string) { if s.activityLogger == nil { return } userID := appcontext.FromGinContext(ctx).UserID err := s.activityLogger.LogLetterCreated(ctx, letterID, userID, letterNumber) if err != nil { // Log error but don't fail the operation } } func (s *LetterServiceImpl) addCreatorAsRecipient(ctx context.Context, letterID uuid.UUID, creatorID uuid.UUID) (*entities.LetterIncomingRecipient, error) { // Check if creator is already a recipient (to avoid duplicates) existingRecipients, err := s.processor.GetBatchRecipientsByUser(ctx, []uuid.UUID{letterID}, creatorID) if err != nil { return nil, err } // If creator is already a recipient, skip if _, exists := existingRecipients[letterID]; exists { return nil, nil } // Create recipient entry for the creator recipient := entities.LetterIncomingRecipient{ ID: uuid.New(), LetterID: letterID, RecipientUserID: &creatorID, Status: entities.RecipientStatusNew, CreatedAt: time.Now(), } // Save the recipient if err := s.recipientProcessor.CreateSingleRecipient(ctx, &recipient); err != nil { // Failed to add creator as recipient return nil, err } return &recipient, nil } func (s *LetterServiceImpl) sendLetterNotifications(ctx context.Context, letter *contract.IncomingLetterResponse, recipients []entities.LetterIncomingRecipient) { for _, recipient := range recipients { if recipient.Status != "completed" { err := s.notificationProcessor.SendIncomingLetterNotification( ctx, letter.ID, *recipient.RecipientUserID, "Surat Masuk", fmt.Sprintf("%s: %s", letter.SenderInstitution.Name, letter.Subject)) if err != nil { // Failed to send notification, continue anyway } } } } func (s *LetterServiceImpl) sendDispositionNotifications(ctx context.Context, letterID uuid.UUID, recipients []entities.LetterIncomingRecipient) { // Get letter details for notification appContext := appcontext.FromGinContext(ctx) letter, err := s.processor.GetIncomingLetterByID(ctx, letterID) if err != nil { return } for _, recipient := range recipients { if recipient.RecipientUserID != nil && recipient.Status != entities.RecipientStatusCompleted { subject := "Surat Masuk" message := fmt.Sprintf("Disposisi surat dari %s: %s", appContext.UserName, letter.Subject) err := s.notificationProcessor.SendIncomingLetterNotification( ctx, letterID, *recipient.RecipientUserID, subject, message) if err != nil { // Failed to send notification, continue anyway } } } } func (s *LetterServiceImpl) GetIncomingLetterByID(ctx context.Context, id uuid.UUID) (*contract.IncomingLetterResponse, error) { return s.processor.GetIncomingLetterByID(ctx, id) } func (s *LetterServiceImpl) ListIncomingLetters(ctx context.Context, req *contract.ListIncomingLettersRequest) (*contract.ListIncomingLettersResponse, error) { appCtx := appcontext.FromGinContext(ctx) userID := appCtx.UserID departmentID := appCtx.DepartmentID filter := repository.ListIncomingLettersFilter{ Status: req.Status, Query: req.Query, DepartmentID: &departmentID, UserID: &userID, IsRead: req.IsRead, PriorityIDs: req.PriorityIDs, IsDispositioned: req.IsDispositioned, IsArchived: req.IsArchived, } letters, total, err := s.processor.ListIncomingLetters(ctx, filter, req.Page, req.Limit) if err != nil { return nil, err } if len(letters) == 0 { return &contract.ListIncomingLettersResponse{ Letters: []contract.IncomingLetterResponse{}, Pagination: transformer.CreatePaginationResponse(int(total), req.Page, req.Limit), TotalUnread: 0, }, nil } letterIDs := make([]uuid.UUID, 0, len(letters)) priorityIDSet := make(map[uuid.UUID]bool) institutionIDSet := make(map[uuid.UUID]bool) for _, letter := range letters { letterIDs = append(letterIDs, letter.ID) if letter.PriorityID != nil { priorityIDSet[*letter.PriorityID] = true } if letter.SenderInstitutionID != nil { institutionIDSet[*letter.SenderInstitutionID] = true } } priorityIDs := make([]uuid.UUID, 0, len(priorityIDSet)) for id := range priorityIDSet { priorityIDs = append(priorityIDs, id) } institutionIDs := make([]uuid.UUID, 0, len(institutionIDSet)) for id := range institutionIDSet { institutionIDs = append(institutionIDs, id) } type batchResult struct { attachments map[uuid.UUID][]entities.LetterIncomingAttachment priorities map[uuid.UUID]*entities.Priority institutions map[uuid.UUID]*entities.Institution recipients map[uuid.UUID]*entities.LetterIncomingRecipient dispostions map[uuid.UUID][]entities.LetterIncomingDisposition err error } resultChan := make(chan batchResult, 1) go func() { result := batchResult{ attachments: make(map[uuid.UUID][]entities.LetterIncomingAttachment), priorities: make(map[uuid.UUID]*entities.Priority), institutions: make(map[uuid.UUID]*entities.Institution), recipients: make(map[uuid.UUID]*entities.LetterIncomingRecipient), dispostions: make(map[uuid.UUID][]entities.LetterIncomingDisposition), } errChan := make(chan error, 5) go func() { var err error result.attachments, err = s.processor.GetBatchAttachments(ctx, letterIDs) errChan <- err }() go func() { var err error result.priorities, err = s.processor.GetBatchPriorities(ctx, priorityIDs) errChan <- err }() go func() { var err error result.institutions, err = s.processor.GetBatchInstitutions(ctx, institutionIDs) errChan <- err }() go func() { var err error result.recipients, err = s.processor.GetBatchRecipientsByUser(ctx, letterIDs, userID) errChan <- err }() go func() { var err error result.dispostions, err = s.processor.GetBatchDispositions(ctx, letterIDs) errChan <- err }() for i := 0; i < 5; i++ { if err := <-errChan; err != nil { // Batch load error, continue anyway } } resultChan <- result }() batchData := <-resultChan respList := make([]contract.IncomingLetterResponse, 0, len(letters)) for _, letter := range letters { attachments := batchData.attachments[letter.ID] if attachments == nil { attachments = []entities.LetterIncomingAttachment{} } var priority *entities.Priority if letter.PriorityID != nil { priority = batchData.priorities[*letter.PriorityID] } var institution *entities.Institution if letter.SenderInstitutionID != nil { institution = batchData.institutions[*letter.SenderInstitutionID] } dispositions := batchData.dispostions[letter.ID] if dispositions == nil { dispositions = []entities.LetterIncomingDisposition{} } isRead := false if recipient, exists := batchData.recipients[letter.ID]; exists && recipient != nil { isRead = recipient.ReadAt != nil log.Printf("Recipient debug: %+v\n", recipient) } resp := transformer.LetterEntityToContract(&letter, attachments, dispositions, priority, institution) resp.IsRead = isRead respList = append(respList, *resp) } totalUnread, _ := s.processor.CountUnreadByUser(ctx, userID) return &contract.ListIncomingLettersResponse{ Letters: respList, Pagination: transformer.CreatePaginationResponse(int(total), req.Page, req.Limit), TotalUnread: totalUnread, }, nil } func (s *LetterServiceImpl) GetLetterUnreadCounts(ctx context.Context) (*contract.LetterUnreadCountResponse, error) { return s.processor.GetLetterUnreadCounts(ctx) } func (s *LetterServiceImpl) MarkIncomingLetterAsRead(ctx context.Context, letterID uuid.UUID) (*contract.MarkLetterReadResponse, error) { return s.processor.MarkIncomingLetterAsRead(ctx, letterID) } func (s *LetterServiceImpl) MarkOutgoingLetterAsRead(ctx context.Context, letterID uuid.UUID) (*contract.MarkLetterReadResponse, error) { return s.processor.MarkOutgoingLetterAsRead(ctx, letterID) } func (s *LetterServiceImpl) UpdateIncomingLetter(ctx context.Context, id uuid.UUID, req *contract.UpdateIncomingLetterRequest) (*contract.IncomingLetterResponse, error) { return s.processor.UpdateIncomingLetter(ctx, id, req) } func (s *LetterServiceImpl) SoftDeleteIncomingLetter(ctx context.Context, id uuid.UUID) error { return s.processor.SoftDeleteIncomingLetter(ctx, id) } func (s *LetterServiceImpl) SearchIncomingLetters(ctx context.Context, req *contract.SearchIncomingLettersRequest) (*contract.SearchIncomingLettersResponse, error) { appCtx := appcontext.FromGinContext(ctx) userID := appCtx.UserID departmentID := appCtx.DepartmentID // Build search filters filters := buildIncomingSearchFilters(req, userID, departmentID) // Execute search with pagination letters, total, err := s.processor.SearchIncomingLetters(ctx, filters, req.Page, req.Limit, req.SortBy, req.SortOrder) if err != nil { return nil, err } // Collect IDs for batch loading letterIDs := make([]uuid.UUID, len(letters)) priorityIDMap := make(map[uuid.UUID]bool) institutionIDMap := make(map[uuid.UUID]bool) for i, letter := range letters { letterIDs[i] = letter.ID if letter.PriorityID != nil { priorityIDMap[*letter.PriorityID] = true } if letter.SenderInstitutionID != nil { institutionIDMap[*letter.SenderInstitutionID] = true } } // Convert maps to slices priorityIDSlice := make([]uuid.UUID, 0, len(priorityIDMap)) for id := range priorityIDMap { priorityIDSlice = append(priorityIDSlice, id) } institutionIDSlice := make([]uuid.UUID, 0, len(institutionIDMap)) for id := range institutionIDMap { institutionIDSlice = append(institutionIDSlice, id) } // Parallel batch loading type batchLoadResult struct { attachments map[uuid.UUID][]entities.LetterIncomingAttachment recipients map[uuid.UUID]*entities.LetterIncomingRecipient priorities map[uuid.UUID]*entities.Priority institutions map[uuid.UUID]*entities.Institution } var result batchLoadResult errChan := make(chan error, 4) // Load attachments go func() { result.attachments, err = s.processor.GetBatchAttachments(ctx, letterIDs) errChan <- err }() // Load recipients for user go func() { result.recipients, err = s.processor.GetBatchRecipientsByUser(ctx, letterIDs, userID) errChan <- err }() // Load priorities go func() { result.priorities, err = s.processor.GetBatchPriorities(ctx, priorityIDSlice) errChan <- err }() // Load institutions go func() { result.institutions, err = s.processor.GetBatchInstitutions(ctx, institutionIDSlice) errChan <- err }() // Wait for all goroutines and check for errors for i := 0; i < 4; i++ { if err := <-errChan; err != nil { return nil, err } } // Transform letters with batch loaded data items := make([]contract.IncomingLetterResponse, len(letters)) for i, letter := range letters { // Attach batch loaded data attachmentResponses := []contract.IncomingLetterAttachmentResponse{} if attachments, ok := result.attachments[letter.ID]; ok { for _, att := range attachments { attachmentResponses = append(attachmentResponses, contract.IncomingLetterAttachmentResponse{ ID: att.ID, FileURL: att.FileURL, FileName: att.FileName, FileType: att.FileType, UploadedAt: att.UploadedAt, }) } } var priorityResp *contract.PriorityResponse if letter.PriorityID != nil { if priority, ok := result.priorities[*letter.PriorityID]; ok { priorityResp = &contract.PriorityResponse{ ID: priority.ID.String(), Name: priority.Name, Level: priority.Level, CreatedAt: priority.CreatedAt, UpdatedAt: priority.UpdatedAt, } } } var institutionResp *contract.InstitutionResponse if letter.SenderInstitutionID != nil { if institution, ok := result.institutions[*letter.SenderInstitutionID]; ok { institutionResp = &contract.InstitutionResponse{ ID: institution.ID.String(), Name: institution.Name, Type: string(institution.Type), Address: institution.Address, ContactPerson: institution.ContactPerson, Phone: institution.Phone, Email: institution.Email, CreatedAt: institution.CreatedAt, UpdatedAt: institution.UpdatedAt, } } } isRead := false if recipient, ok := result.recipients[letter.ID]; ok && recipient.ReadAt != nil { isRead = true } items[i] = contract.IncomingLetterResponse{ ID: letter.ID, LetterNumber: letter.LetterNumber, ReferenceNumber: letter.ReferenceNumber, Subject: letter.Subject, Description: letter.Description, Priority: priorityResp, SenderInstitution: institutionResp, SenderName: letter.SenderName, ReceivedDate: letter.ReceivedDate, DueDate: letter.DueDate, Status: string(letter.Status), CreatedBy: letter.CreatedBy, CreatedAt: letter.CreatedAt, UpdatedAt: letter.UpdatedAt, Attachments: attachmentResponses, IsRead: isRead, } } return &contract.SearchIncomingLettersResponse{ Letters: items, TotalCount: total, Page: req.Page, Limit: req.Limit, }, nil } func buildIncomingSearchFilters(req *contract.SearchIncomingLettersRequest, userID, departmentID uuid.UUID) map[string]interface{} { filters := make(map[string]interface{}) if req.Query != "" { filters["query"] = req.Query } if req.LetterNumber != "" { filters["letter_number"] = req.LetterNumber } if req.Subject != "" { filters["subject"] = req.Subject } if req.Status != "" { filters["status"] = req.Status } if req.PriorityID != nil { filters["priority_id"] = *req.PriorityID } if req.InstitutionID != nil { filters["sender_institution_id"] = *req.InstitutionID } if req.CreatedBy != nil { filters["created_by"] = *req.CreatedBy } if req.DateFrom != nil { filters["date_from"] = *req.DateFrom } if req.DateTo != nil { filters["date_to"] = *req.DateTo } // Add user/department context filters filters["user_context"] = map[string]interface{}{ "user_id": userID, "department_id": departmentID, } return filters } func (s *LetterServiceImpl) CreateDispositions(ctx context.Context, req *contract.CreateLetterDispositionRequest) (*contract.ListDispositionsResponse, error) { log.Printf("[DEBUG] CreateDispositions START - LetterID: %s\n", req.LetterID.String()) userID := appcontext.FromGinContext(ctx).UserID req.CreatedBy = userID if req.FromDepartment == uuid.Nil { req.FromDepartment = appcontext.FromGinContext(ctx).DepartmentID } var result *contract.ListDispositionsResponse var recipients []entities.LetterIncomingRecipient err := s.txManager.WithTransaction(ctx, func(txCtx context.Context) error { var err error result, err = s.processor.CreateDispositions(txCtx, req) if err != nil { return err } if len(req.ToDepartmentIDs) > 0 && s.recipientProcessor != nil { recipients, err = s.recipientProcessor.CreateRecipients(txCtx, req.LetterID, req.ToDepartmentIDs) if err != nil { return err } } if s.activityLogger != nil && result != nil && len(result.Dispositions) > 0 { if err := s.activityLogger.LogLetterDispositionStatusUpdate(txCtx, req.LetterID, userID, "disposition_created"); err != nil { } } return nil }) if err != nil { return nil, err } // Send notifications to newly created recipients asynchronously if s.notificationProcessor != nil { // Send notifications to newly created recipients if len(recipients) > 0 { go s.sendDispositionNotifications(context.Background(), req.LetterID, recipients) } // Send notification to letter creator about new disposition go s.sendDispositionCreatorNotification(context.Background(), req.LetterID, userID) } return result, nil } func (s *LetterServiceImpl) GetEnhancedDispositionsByLetter(ctx context.Context, letterID uuid.UUID) (*contract.ListEnhancedDispositionsResponse, error) { return s.processor.GetEnhancedDispositionsByLetter(ctx, letterID) } func (s *LetterServiceImpl) CreateDiscussion(ctx context.Context, letterID uuid.UUID, req *contract.CreateLetterDiscussionRequest) (*contract.LetterDiscussionResponse, error) { userID := appcontext.FromGinContext(ctx).UserID var result *contract.LetterDiscussionResponse err := s.txManager.WithTransaction(ctx, func(txCtx context.Context) error { var err error result, err = s.processor.CreateDiscussion(txCtx, letterID, req) if err != nil { return err } // Log activity for discussion creation if s.activityLogger != nil && result != nil { if err := s.activityLogger.LogLetterDispositionStatusUpdate(txCtx, letterID, userID, "discussion_created"); err != nil { // Don't fail the transaction for logging errors } } return nil }) if err != nil { return nil, err } // Send notifications to mentioned users asynchronously if s.notificationProcessor != nil && req.Mentions != nil { go s.sendDiscussionMentionNotifications(context.Background(), letterID, userID, req.Mentions, req.Message) } return result, nil } func (s *LetterServiceImpl) UpdateDiscussion(ctx context.Context, letterID uuid.UUID, discussionID uuid.UUID, req *contract.UpdateLetterDiscussionRequest) (*contract.LetterDiscussionResponse, error) { userID := appcontext.FromGinContext(ctx).UserID var result *contract.LetterDiscussionResponse err := s.txManager.WithTransaction(ctx, func(txCtx context.Context) error { var err error var oldMessage string result, oldMessage, err = s.processor.UpdateDiscussion(txCtx, letterID, discussionID, req) if err != nil { return err } // Log activity for discussion update (could use oldMessage for more detailed logging) if s.activityLogger != nil && result != nil { // Create a simple activity log - oldMessage could be included in a more detailed log _ = oldMessage // Mark as intentionally unused for now if err := s.activityLogger.LogLetterDispositionStatusUpdate(txCtx, letterID, userID, "discussion_updated"); err != nil { // Don't fail the transaction for logging errors } } return nil }) if err != nil { return nil, err } return result, nil } func (s *LetterServiceImpl) GetDepartmentDispositionStatus(ctx context.Context, req *contract.GetDepartmentDispositionStatusRequest) (*contract.ListDepartmentDispositionStatusResponse, error) { return s.processor.GetDepartmentDispositionStatus(ctx, req) } func (s *LetterServiceImpl) UpdateDispositionStatus(ctx context.Context, req *contract.UpdateDispositionStatusRequest) (*contract.DepartmentDispositionStatusResponse, error) { var result *contract.DepartmentDispositionStatusResponse err := s.txManager.WithTransaction(ctx, func(txCtx context.Context) error { var err error result, err = s.processor.UpdateDispositionStatus(txCtx, req) if err != nil { return err } // Log activity for disposition status update if s.activityLogger != nil && result != nil { userID := appcontext.FromGinContext(txCtx).UserID if err := s.activityLogger.LogLetterDispositionStatusUpdate(txCtx, req.LetterIncomingID, userID, req.Status); err != nil { // Don't fail the transaction for logging errors } } return nil }) if err != nil { return nil, err } // Send notification to letter creator asynchronously if s.notificationProcessor != nil && result != nil { go s.sendDispositionStatusUpdateNotification(context.Background(), req.LetterIncomingID, req.Status) } return result, nil } func (s *LetterServiceImpl) GetLetterCTA(ctx context.Context, letterID uuid.UUID) (*contract.LetterCTAResponse, error) { departmentID := appcontext.FromGinContext(ctx).DepartmentID return s.processor.GetLetterCTA(ctx, letterID, departmentID) } func (s *LetterServiceImpl) BulkArchiveIncomingLetters(ctx context.Context, letterIDs []uuid.UUID) (*contract.BulkArchiveLettersResponse, error) { userID := appcontext.FromGinContext(ctx).UserID // Archive the letters themselves archivedCount, err := s.processor.BulkArchiveIncomingLettersForUser(ctx, letterIDs, userID) if err != nil { return nil, err } return &contract.BulkArchiveLettersResponse{ Success: true, Message: "Letters archived successfully", ArchivedCount: int(archivedCount), }, 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) if len(userIDs) == 0 { return } // Get letter details for notification letter, err := s.processor.GetIncomingLetterByID(ctx, letterID) if err != nil { return } // Get sender user name (you might need to implement this) appContext := appcontext.FromGinContext(ctx) senderName := appContext.UserName // or get from user service // Send notification to each mentioned user for _, mentionedUserID := range userIDs { // Don't send notification to the sender themselves if mentionedUserID == senderUserID { continue } subject := "Anda Disebutkan dalam Diskusi" notificationMessage := fmt.Sprintf("%s menyebutkan Anda dalam diskusi surat: %s", senderName, letter.Subject) err := s.notificationProcessor.SendIncomingLetterNotification( ctx, letterID, mentionedUserID, subject, notificationMessage) if err != nil { // Log error but continue with other notifications } } } func (s *LetterServiceImpl) extractUserIDsFromMentions(mentions map[string]interface{}) []uuid.UUID { userIDs := make([]uuid.UUID, 0) if userIDsInterface, exists := mentions["user_ids"]; exists { switch userIDsValue := userIDsInterface.(type) { case []interface{}: for _, userIDInterface := range userIDsValue { if userIDStr, ok := userIDInterface.(string); ok { if userID, err := uuid.Parse(userIDStr); err == nil { userIDs = append(userIDs, userID) } } } case []string: for _, userIDStr := range userIDsValue { if userID, err := uuid.Parse(userIDStr); err == nil { userIDs = append(userIDs, userID) } } } } return userIDs } func (s *LetterServiceImpl) sendDispositionCreatorNotification(ctx context.Context, letterID uuid.UUID, dispositionCreatorID uuid.UUID) { // Get letter details letter, err := s.processor.GetIncomingLetterByID(ctx, letterID) if err != nil { return } fmt.Printf("[DEBUG] Starting sendDispositionCreatorNotification for letterID: %s\n", letterID.String()) fmt.Printf("[DEBUG] Successfully retrieved letter: %s\n", letter.Subject) fmt.Printf("[DEBUG] Successfully retrieved letter: %s\n", letter.CreatedBy) letterCreatorID := letter.CreatedBy // Don't send notification if the disposition creator is the same as letter creator if letterCreatorID == dispositionCreatorID { return } // Get disposition creator name from context appContext := appcontext.FromGinContext(ctx) dispositionCreatorName := appContext.UserName subject := "Disposisi Baru pada Surat Anda" message := fmt.Sprintf("Surat yang Anda buat telah didisposisikan %s: %s", dispositionCreatorName, letter.Subject) err = s.notificationProcessor.SendIncomingLetterNotification( ctx, letterID, letterCreatorID, subject, message) if err != nil { // Log error but don't fail the operation // You might want to add proper logging here } } func (s *LetterServiceImpl) sendDispositionStatusUpdateNotification(ctx context.Context, letterID uuid.UUID, newStatus string) { // Get letter details letter, err := s.processor.GetIncomingLetterByID(ctx, letterID) if err != nil { // Log error but don't fail return } // Get current user context (the one updating the status) appContext := appcontext.FromGinContext(ctx) updaterUserID := appContext.UserID updaterName := appContext.UserName letterCreatorID := letter.CreatedBy // Don't send notification if the updater is the same as letter creator if letterCreatorID == updaterUserID { return } // Create status-specific notification message var statusMessage string switch newStatus { case "pending": statusMessage = "sedang menunggu" case "in_progress": statusMessage = "sedang diproses" case "completed": statusMessage = "telah diselesaikan" case "cancelled": statusMessage = "dibatalkan" default: statusMessage = fmt.Sprintf("diubah statusnya menjadi %s", newStatus) } subject := "Status Disposisi Surat Diperbarui" message := fmt.Sprintf("Disposisi surat '%s' %s %s", letter.Subject, statusMessage, updaterName) err = s.notificationProcessor.SendIncomingLetterNotification( ctx, letterID, letterCreatorID, subject, message) if err != nil { // Log error but don't fail the operation // You might want to add proper logging here } }