dukcapil/internal/processor/letter_processor.go
2025-09-08 12:24:37 +07:00

568 lines
20 KiB
Go

package processor
import (
"context"
"time"
"eslogad-be/internal/appcontext"
"eslogad-be/internal/contract"
"eslogad-be/internal/entities"
"eslogad-be/internal/repository"
"eslogad-be/internal/transformer"
"github.com/google/uuid"
)
type LetterProcessorImpl struct {
letterRepo *repository.LetterIncomingRepository
attachRepo *repository.LetterIncomingAttachmentRepository
txManager *repository.TxManager
activity *ActivityLogProcessorImpl
dispositionRepo *repository.LetterIncomingDispositionRepository
dispositionDeptRepo *repository.LetterIncomingDispositionDepartmentRepository
dispositionActionSelRepo *repository.LetterDispositionActionSelectionRepository
dispositionNoteRepo *repository.DispositionNoteRepository
discussionRepo *repository.LetterDiscussionRepository
settingRepo *repository.AppSettingRepository
recipientRepo *repository.LetterIncomingRecipientRepository
outgoingRecipientRepo *repository.LetterOutgoingRecipientRepository
departmentRepo *repository.DepartmentRepository
userDeptRepo *repository.UserDepartmentRepository
priorityRepo *repository.PriorityRepository
institutionRepo *repository.InstitutionRepository
dispActionRepo *repository.DispositionActionRepository
dispoRoutes *repository.DispositionRouteRepository
numberGenerator *LetterNumberGeneratorImpl
}
func NewLetterProcessor(letterRepo *repository.LetterIncomingRepository, attachRepo *repository.LetterIncomingAttachmentRepository, txManager *repository.TxManager, activity *ActivityLogProcessorImpl, dispRepo *repository.LetterIncomingDispositionRepository, dispDeptRepo *repository.LetterIncomingDispositionDepartmentRepository, dispSelRepo *repository.LetterDispositionActionSelectionRepository, noteRepo *repository.DispositionNoteRepository, discussionRepo *repository.LetterDiscussionRepository,
settingRepo *repository.AppSettingRepository,
recipientRepo *repository.LetterIncomingRecipientRepository,
outgoingRecipientRepo *repository.LetterOutgoingRecipientRepository,
departmentRepo *repository.DepartmentRepository,
userDeptRepo *repository.UserDepartmentRepository,
priorityRepo *repository.PriorityRepository,
institutionRepo *repository.InstitutionRepository,
dispActionRepo *repository.DispositionActionRepository,
numberGenerator *LetterNumberGeneratorImpl,
dispoRoutes *repository.DispositionRouteRepository) *LetterProcessorImpl {
return &LetterProcessorImpl{letterRepo: letterRepo, attachRepo: attachRepo, txManager: txManager,
activity: activity, dispositionRepo: dispRepo, dispositionDeptRepo: dispDeptRepo,
dispositionActionSelRepo: dispSelRepo, dispositionNoteRepo: noteRepo,
discussionRepo: discussionRepo, settingRepo: settingRepo, recipientRepo: recipientRepo,
outgoingRecipientRepo: outgoingRecipientRepo,
departmentRepo: departmentRepo, userDeptRepo: userDeptRepo, priorityRepo: priorityRepo,
institutionRepo: institutionRepo, dispActionRepo: dispActionRepo, numberGenerator: numberGenerator,
dispoRoutes: dispoRoutes}
}
func (p *LetterProcessorImpl) CreateIncomingLetter(ctx context.Context, req *contract.CreateIncomingLetterRequest) (*contract.IncomingLetterResponse, error) {
userID := appcontext.FromGinContext(ctx).UserID
entity := &entities.LetterIncoming{
LetterNumber: req.LetterNumber,
ReferenceNumber: req.ReferenceNumber,
Subject: req.Subject,
Description: req.Description,
PriorityID: req.PriorityID,
SenderInstitutionID: req.SenderInstitutionID,
ReceivedDate: req.ReceivedDate,
DueDate: req.DueDate,
Status: entities.LetterIncomingStatusNew,
CreatedBy: userID,
}
if err := p.letterRepo.Create(ctx, entity); err != nil {
return nil, err
}
if err := p.createAttachments(ctx, entity.ID, req.Attachments, userID); err != nil {
return nil, err
}
return p.buildLetterResponse(ctx, entity)
}
func (p *LetterProcessorImpl) GetIncomingLetterByID(ctx context.Context, id uuid.UUID) (*contract.IncomingLetterResponse, error) {
// Get current user ID from context
userID := appcontext.FromGinContext(ctx).UserID
entity, err := p.letterRepo.Get(ctx, id)
if err != nil {
return nil, err
}
atts, _ := p.attachRepo.ListByLetter(ctx, id)
var pr *entities.Priority
if entity.PriorityID != nil && p.priorityRepo != nil {
if got, err := p.priorityRepo.Get(ctx, *entity.PriorityID); err == nil {
pr = got
}
}
var inst *entities.Institution
if entity.SenderInstitutionID != nil && p.institutionRepo != nil {
if got, err := p.institutionRepo.Get(ctx, *entity.SenderInstitutionID); err == nil {
inst = got
}
}
// Check if letter is read by current user
isRead := false
if p.recipientRepo != nil {
if recipient, err := p.recipientRepo.GetByLetterAndUser(ctx, id, userID); err == nil {
isRead = recipient.ReadAt != nil
}
}
resp := transformer.LetterEntityToContract(entity, atts, pr, inst)
resp.IsRead = isRead
// Include created_by if the current user is the creator
if entity.CreatedBy == userID {
resp.CreatedBy = entity.CreatedBy
}
return resp, nil
}
func (p *LetterProcessorImpl) GetLetterUnreadCounts(ctx context.Context) (*contract.LetterUnreadCountResponse, error) {
userID := appcontext.FromGinContext(ctx).UserID
incomingUnread := 0
if p.recipientRepo != nil {
if count, err := p.recipientRepo.CountUnreadByUser(ctx, userID); err == nil {
incomingUnread = count
}
}
outgoingUnread := 0
if p.outgoingRecipientRepo != nil {
if count, err := p.outgoingRecipientRepo.CountUnreadByUser(ctx, userID); err == nil {
outgoingUnread = count
}
}
response := &contract.LetterUnreadCountResponse{}
response.IncomingLetter.Unread = incomingUnread
response.OutgoingLetter.Unread = outgoingUnread
return response, nil
}
func (p *LetterProcessorImpl) MarkIncomingLetterAsRead(ctx context.Context, letterID uuid.UUID) (*contract.MarkLetterReadResponse, error) {
// Get current user ID from context
userID := appcontext.FromGinContext(ctx).UserID
// Mark the letter as read for the current user
if p.recipientRepo != nil {
if err := p.recipientRepo.MarkAsRead(ctx, letterID, userID); err != nil {
return &contract.MarkLetterReadResponse{
Success: false,
Message: "Failed to mark letter as read",
}, err
}
}
return &contract.MarkLetterReadResponse{
Success: true,
Message: "Letter marked as read successfully",
}, nil
}
func (p *LetterProcessorImpl) MarkOutgoingLetterAsRead(ctx context.Context, letterID uuid.UUID) (*contract.MarkLetterReadResponse, error) {
// Get current user ID from context
userID := appcontext.FromGinContext(ctx).UserID
// Mark the letter as read for the current user
if p.outgoingRecipientRepo != nil {
if err := p.outgoingRecipientRepo.MarkAsRead(ctx, letterID, userID); err != nil {
return &contract.MarkLetterReadResponse{
Success: false,
Message: "Failed to mark letter as read",
}, err
}
}
return &contract.MarkLetterReadResponse{
Success: true,
Message: "Letter marked as read successfully",
}, nil
}
func (p *LetterProcessorImpl) ListIncomingLetters(ctx context.Context, filter repository.ListIncomingLettersFilter, page, limit int) ([]entities.LetterIncoming, int64, error) {
// Just fetch the raw data
return p.letterRepo.List(ctx, filter, limit, (page-1)*limit)
}
func (p *LetterProcessorImpl) GetBatchAttachments(ctx context.Context, letterIDs []uuid.UUID) (map[uuid.UUID][]entities.LetterIncomingAttachment, error) {
if p.attachRepo == nil || len(letterIDs) == 0 {
return make(map[uuid.UUID][]entities.LetterIncomingAttachment), nil
}
return p.attachRepo.ListByLetterIDs(ctx, letterIDs)
}
func (p *LetterProcessorImpl) 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)
}
func (p *LetterProcessorImpl) 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)
}
func (p *LetterProcessorImpl) GetBatchRecipientsByUser(ctx context.Context, letterIDs []uuid.UUID, userID uuid.UUID) (map[uuid.UUID]*entities.LetterIncomingRecipient, error) {
if p.recipientRepo == nil || len(letterIDs) == 0 {
return make(map[uuid.UUID]*entities.LetterIncomingRecipient), nil
}
return p.recipientRepo.GetByLetterIDsAndUser(ctx, letterIDs, userID)
}
func (p *LetterProcessorImpl) CountUnreadByUser(ctx context.Context, userID uuid.UUID) (int, error) {
if p.recipientRepo == nil {
return 0, nil
}
return p.recipientRepo.CountUnreadByUser(ctx, userID)
}
func (p *LetterProcessorImpl) UpdateIncomingLetter(ctx context.Context, id uuid.UUID, req *contract.UpdateIncomingLetterRequest) (*contract.IncomingLetterResponse, error) {
var out *contract.IncomingLetterResponse
err := p.txManager.WithTransaction(ctx, func(txCtx context.Context) error {
entity, err := p.letterRepo.Get(txCtx, id)
if err != nil {
return err
}
fromStatus := string(entity.Status)
if req.ReferenceNumber != nil {
entity.ReferenceNumber = req.ReferenceNumber
}
if req.Subject != nil {
entity.Subject = *req.Subject
}
if req.Description != nil {
entity.Description = req.Description
}
if req.PriorityID != nil {
entity.PriorityID = req.PriorityID
}
if req.SenderInstitutionID != nil {
entity.SenderInstitutionID = req.SenderInstitutionID
}
if req.ReceivedDate != nil {
entity.ReceivedDate = *req.ReceivedDate
}
if req.DueDate != nil {
entity.DueDate = req.DueDate
}
if req.Status != nil {
entity.Status = entities.LetterIncomingStatus(*req.Status)
}
if err := p.letterRepo.Update(txCtx, entity); err != nil {
return err
}
toStatus := string(entity.Status)
if p.activity != nil && fromStatus != toStatus {
userID := appcontext.FromGinContext(txCtx).UserID
action := "status.changed"
if err := p.activity.Log(txCtx, id, action, &userID, nil, nil, nil, &fromStatus, &toStatus, map[string]interface{}{}); err != nil {
return err
}
}
atts, _ := p.attachRepo.ListByLetter(txCtx, id)
var pr *entities.Priority
if entity.PriorityID != nil && p.priorityRepo != nil {
if got, err := p.priorityRepo.Get(txCtx, *entity.PriorityID); err == nil {
pr = got
}
}
var inst *entities.Institution
if entity.SenderInstitutionID != nil && p.institutionRepo != nil {
if got, err := p.institutionRepo.Get(txCtx, *entity.SenderInstitutionID); err == nil {
inst = got
}
}
out = transformer.LetterEntityToContract(entity, atts, pr, inst)
return nil
})
if err != nil {
return nil, err
}
return out, nil
}
func (p *LetterProcessorImpl) SoftDeleteIncomingLetter(ctx context.Context, id uuid.UUID) error {
return p.txManager.WithTransaction(ctx, func(txCtx context.Context) error {
if err := p.letterRepo.SoftDelete(txCtx, id); err != nil {
return err
}
if p.activity != nil {
userID := appcontext.FromGinContext(txCtx).UserID
action := "letter.deleted"
if err := p.activity.Log(txCtx, id, action, &userID, nil, nil, nil, nil, nil, map[string]interface{}{}); err != nil {
return err
}
}
return nil
})
}
func (p *LetterProcessorImpl) CreateDispositions(ctx context.Context, req *contract.CreateLetterDispositionRequest) (*contract.ListDispositionsResponse, error) {
var out *contract.ListDispositionsResponse
err := p.txManager.WithTransaction(ctx, func(txCtx context.Context) error {
userID := appcontext.FromGinContext(txCtx).UserID
existingDispDepts, err := p.dispositionDeptRepo.GetByLetterAndDepartment(txCtx, req.LetterID, req.FromDepartment)
if err == nil && len(existingDispDepts) > 0 {
for _, existingDispDept := range existingDispDepts {
if existingDispDept.Status == entities.DispositionDepartmentStatusPending {
existingDispDept.Status = entities.DispositionDepartmentStatusDispositioned
if err := p.dispositionDeptRepo.Update(txCtx, &existingDispDept); err != nil {
return err
}
}
}
}
disp := entities.LetterIncomingDisposition{
LetterID: req.LetterID,
DepartmentID: &req.FromDepartment,
Notes: req.Notes,
CreatedBy: userID,
}
if err := p.dispositionRepo.Create(txCtx, &disp); err != nil {
return err
}
var dispDepartments []entities.LetterIncomingDispositionDepartment
for _, toDept := range req.ToDepartmentIDs {
dispDepartments = append(dispDepartments, entities.LetterIncomingDispositionDepartment{
LetterIncomingDispositionID: disp.ID,
LetterIncomingID: req.LetterID,
DepartmentID: toDept,
Status: entities.DispositionDepartmentStatusPending,
})
}
if err := p.dispositionDeptRepo.CreateBulk(txCtx, dispDepartments); err != nil {
return err
}
if len(req.SelectedActions) > 0 {
selections := make([]entities.LetterDispositionActionSelection, 0, len(req.SelectedActions))
for _, sel := range req.SelectedActions {
selections = append(selections, entities.LetterDispositionActionSelection{
DispositionID: disp.ID,
ActionID: sel.ActionID,
Note: sel.Note,
CreatedBy: userID,
})
}
if err := p.dispositionActionSelRepo.CreateBulk(txCtx, selections); err != nil {
return err
}
}
if p.activity != nil {
action := "disposition.created"
ctxMap := map[string]interface{}{"to_department_id": dispDepartments}
if err := p.activity.Log(txCtx, req.LetterID, action, &userID, nil, nil, &disp.ID, nil, nil, ctxMap); err != nil {
return err
}
}
out = &contract.ListDispositionsResponse{Dispositions: []contract.DispositionResponse{transformer.DispoToContract(disp)}}
return nil
})
if err != nil {
return nil, err
}
return out, nil
}
func (p *LetterProcessorImpl) ListDispositionsByLetter(ctx context.Context, letterID uuid.UUID) (*contract.ListDispositionsResponse, error) {
list, err := p.dispositionRepo.ListByLetter(ctx, letterID)
if err != nil {
return nil, err
}
return &contract.ListDispositionsResponse{Dispositions: transformer.DispositionsToContract(list)}, nil
}
func (p *LetterProcessorImpl) GetEnhancedDispositionsByLetter(ctx context.Context, letterID uuid.UUID) (*contract.ListEnhancedDispositionsResponse, error) {
// Get dispositions with all related data preloaded in a single query
dispositions, err := p.dispositionRepo.ListByLetter(ctx, letterID)
if err != nil {
return nil, err
}
// Get discussions with preloaded user profiles
discussions, err := p.discussionRepo.ListByLetter(ctx, letterID)
if err != nil {
return nil, err
}
// Extract all mentioned user IDs from discussions for efficient batch fetching
var mentionedUserIDs []uuid.UUID
mentionedUserIDsMap := make(map[uuid.UUID]bool)
for _, discussion := range discussions {
if discussion.Mentions != nil {
mentions := map[string]interface{}(discussion.Mentions)
if userIDs, ok := mentions["user_ids"]; ok {
if userIDList, ok := userIDs.([]interface{}); ok {
for _, userID := range userIDList {
if userIDStr, ok := userID.(string); ok {
if userUUID, err := uuid.Parse(userIDStr); err == nil {
if !mentionedUserIDsMap[userUUID] {
mentionedUserIDsMap[userUUID] = true
mentionedUserIDs = append(mentionedUserIDs, userUUID)
}
}
}
}
}
}
}
}
// Fetch all mentioned users in a single batch query
var mentionedUsers []entities.User
if len(mentionedUserIDs) > 0 {
mentionedUsers, err = p.discussionRepo.GetUsersByIDs(ctx, mentionedUserIDs)
if err != nil {
return nil, err
}
}
// Transform dispositions
enhancedDispositions := transformer.EnhancedDispositionsWithPreloadedDataToContract(dispositions)
// Transform discussions with mentioned users
enhancedDiscussions := transformer.DiscussionsWithPreloadedDataToContract(discussions, mentionedUsers)
return &contract.ListEnhancedDispositionsResponse{
Dispositions: enhancedDispositions,
Discussions: enhancedDiscussions,
}, nil
}
func (p *LetterProcessorImpl) CreateDiscussion(ctx context.Context, letterID uuid.UUID, req *contract.CreateLetterDiscussionRequest) (*contract.LetterDiscussionResponse, error) {
var out *contract.LetterDiscussionResponse
err := p.txManager.WithTransaction(ctx, func(txCtx context.Context) error {
userID := appcontext.FromGinContext(txCtx).UserID
mentions := entities.JSONB(nil)
if req.Mentions != nil {
mentions = entities.JSONB(req.Mentions)
}
disc := &entities.LetterDiscussion{ID: uuid.New(), LetterID: letterID, ParentID: req.ParentID, UserID: userID, Message: req.Message, Mentions: mentions}
if err := p.discussionRepo.Create(txCtx, disc); err != nil {
return err
}
if p.activity != nil {
action := "discussion.created"
tgt := "discussion"
ctxMap := map[string]interface{}{"message": req.Message, "parent_id": req.ParentID}
if err := p.activity.Log(txCtx, letterID, action, &userID, nil, &tgt, &disc.ID, nil, nil, ctxMap); err != nil {
return err
}
}
out = transformer.DiscussionEntityToContract(disc)
return nil
})
if err != nil {
return nil, err
}
return out, nil
}
func (p *LetterProcessorImpl) UpdateDiscussion(ctx context.Context, letterID uuid.UUID, discussionID uuid.UUID, req *contract.UpdateLetterDiscussionRequest) (*contract.LetterDiscussionResponse, error) {
var out *contract.LetterDiscussionResponse
err := p.txManager.WithTransaction(ctx, func(txCtx context.Context) error {
disc, err := p.discussionRepo.Get(txCtx, discussionID)
if err != nil {
return err
}
oldMessage := disc.Message
disc.Message = req.Message
if req.Mentions != nil {
disc.Mentions = entities.JSONB(req.Mentions)
}
now := time.Now()
disc.EditedAt = &now
if err := p.discussionRepo.Update(txCtx, disc); err != nil {
return err
}
if p.activity != nil {
userID := appcontext.FromGinContext(txCtx).UserID
action := "discussion.updated"
tgt := "discussion"
ctxMap := map[string]interface{}{"old_message": oldMessage, "new_message": req.Message}
if err := p.activity.Log(txCtx, letterID, action, &userID, nil, &tgt, &disc.ID, nil, nil, ctxMap); err != nil {
return err
}
}
out = transformer.DiscussionEntityToContract(disc)
return nil
})
if err != nil {
return nil, err
}
return out, nil
}
func (p *LetterProcessorImpl) createAttachments(ctx context.Context, letterID uuid.UUID, attachments []contract.CreateIncomingLetterAttachment, userID uuid.UUID) error {
if len(attachments) == 0 {
return nil
}
attachmentEntities := make([]entities.LetterIncomingAttachment, 0, len(attachments))
for _, a := range attachments {
attachmentEntities = append(attachmentEntities, entities.LetterIncomingAttachment{
LetterID: letterID,
FileURL: a.FileURL,
FileName: a.FileName,
FileType: a.FileType,
UploadedBy: &userID,
})
}
if err := p.attachRepo.CreateBulk(ctx, attachmentEntities); err != nil {
return err
}
// Attachment logging will be handled by service layer
return nil
}
func (p *LetterProcessorImpl) buildLetterResponse(ctx context.Context, entity *entities.LetterIncoming) (*contract.IncomingLetterResponse, error) {
savedAttachments, _ := p.attachRepo.ListByLetter(ctx, entity.ID)
var pr *entities.Priority
if entity.PriorityID != nil && p.priorityRepo != nil {
if got, err := p.priorityRepo.Get(ctx, *entity.PriorityID); err == nil {
pr = got
}
}
var inst *entities.Institution
if entity.SenderInstitutionID != nil && p.institutionRepo != nil {
if got, err := p.institutionRepo.Get(ctx, *entity.SenderInstitutionID); err == nil {
inst = got
}
}
return transformer.LetterEntityToContract(entity, savedAttachments, pr, inst), nil
}
func (p *LetterProcessorImpl) BulkArchiveIncomingLetters(ctx context.Context, letterIDs []uuid.UUID) (int64, error) {
return p.letterRepo.BulkArchive(ctx, letterIDs)
}
// 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)
}