package processor import ( "context" "fmt" "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 // new repos for dispositions dispositionRepo *repository.LetterDispositionRepository dispositionActionSelRepo *repository.LetterDispositionActionSelectionRepository dispositionNoteRepo *repository.DispositionNoteRepository // discussion repo discussionRepo *repository.LetterDiscussionRepository // settings and recipients settingRepo *repository.AppSettingRepository recipientRepo *repository.LetterIncomingRecipientRepository departmentRepo *repository.DepartmentRepository userDeptRepo *repository.UserDepartmentRepository } func NewLetterProcessor(letterRepo *repository.LetterIncomingRepository, attachRepo *repository.LetterIncomingAttachmentRepository, txManager *repository.TxManager, activity *ActivityLogProcessorImpl, dispRepo *repository.LetterDispositionRepository, dispSelRepo *repository.LetterDispositionActionSelectionRepository, noteRepo *repository.DispositionNoteRepository, discussionRepo *repository.LetterDiscussionRepository, settingRepo *repository.AppSettingRepository, recipientRepo *repository.LetterIncomingRecipientRepository, departmentRepo *repository.DepartmentRepository, userDeptRepo *repository.UserDepartmentRepository) *LetterProcessorImpl { return &LetterProcessorImpl{letterRepo: letterRepo, attachRepo: attachRepo, txManager: txManager, activity: activity, dispositionRepo: dispRepo, dispositionActionSelRepo: dispSelRepo, dispositionNoteRepo: noteRepo, discussionRepo: discussionRepo, settingRepo: settingRepo, recipientRepo: recipientRepo, departmentRepo: departmentRepo, userDeptRepo: userDeptRepo} } func (p *LetterProcessorImpl) CreateIncomingLetter(ctx context.Context, req *contract.CreateIncomingLetterRequest) (*contract.IncomingLetterResponse, error) { var result *contract.IncomingLetterResponse err := p.txManager.WithTransaction(ctx, func(txCtx context.Context) error { userID := appcontext.FromGinContext(txCtx).UserID prefix := "ESLI" seq := 0 if s, err := p.settingRepo.Get(txCtx, contract.SettingIncomingLetterPrefix); err == nil { if v, ok := s.Value["value"].(string); ok && v != "" { prefix = v } } if s, err := p.settingRepo.Get(txCtx, contract.SettingIncomingLetterSequence); err == nil { if v, ok := s.Value["value"].(float64); ok { seq = int(v) } } seq = seq + 1 letterNumber := fmt.Sprintf("%s%04d", prefix, seq) entity := &entities.LetterIncoming{ 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, } entity.LetterNumber = letterNumber if err := p.letterRepo.Create(txCtx, entity); err != nil { return err } _ = p.settingRepo.Upsert(txCtx, contract.SettingIncomingLetterSequence, entities.JSONB{"value": seq}) defaultDeptCodes := []string{} if s, err := p.settingRepo.Get(txCtx, contract.SettingIncomingLetterRecipients); err == nil { if arr, ok := s.Value["department_codes"].([]interface{}); ok { for _, it := range arr { if str, ok := it.(string); ok { defaultDeptCodes = append(defaultDeptCodes, str) } } } } // resolve department codes to ids using repository depIDs := make([]uuid.UUID, 0, len(defaultDeptCodes)) for _, code := range defaultDeptCodes { dep, err := p.departmentRepo.GetByCode(txCtx, code) if err != nil { continue } depIDs = append(depIDs, dep.ID) } // query user memberships for all departments at once userMemberships, _ := p.userDeptRepo.ListActiveByDepartmentIDs(txCtx, depIDs) // build recipients: one department recipient per department + one user recipient per membership recipients := make([]entities.LetterIncomingRecipient, 0, len(depIDs)+len(userMemberships)) // department recipients for _, depID := range depIDs { id := depID recipients = append(recipients, entities.LetterIncomingRecipient{LetterID: entity.ID, RecipientDepartmentID: &id, Status: entities.RecipientStatusNew}) } // user recipients for _, row := range userMemberships { uid := row.UserID recipients = append(recipients, entities.LetterIncomingRecipient{LetterID: entity.ID, RecipientUserID: &uid, Status: entities.RecipientStatusNew}) } if len(recipients) > 0 { if err := p.recipientRepo.CreateBulk(txCtx, recipients); err != nil { return err } } if p.activity != nil { action := "letter.created" if err := p.activity.Log(txCtx, entity.ID, action, &userID, nil, nil, nil, nil, nil, map[string]interface{}{"letter_number": letterNumber}); err != nil { return err } } attachments := make([]entities.LetterIncomingAttachment, 0, len(req.Attachments)) for _, a := range req.Attachments { attachments = append(attachments, entities.LetterIncomingAttachment{LetterID: entity.ID, FileURL: a.FileURL, FileName: a.FileName, FileType: a.FileType, UploadedBy: &userID}) } if len(attachments) > 0 { if err := p.attachRepo.CreateBulk(txCtx, attachments); err != nil { return err } if p.activity != nil { action := "attachment.uploaded" for _, a := range attachments { ctxMap := map[string]interface{}{"file_name": a.FileName, "file_type": a.FileType} if err := p.activity.Log(txCtx, entity.ID, action, &userID, nil, nil, nil, nil, nil, ctxMap); err != nil { return err } } } } savedAttachments, _ := p.attachRepo.ListByLetter(txCtx, entity.ID) result = transformer.LetterEntityToContract(entity, savedAttachments) return nil }) if err != nil { return nil, err } return result, nil } func (p *LetterProcessorImpl) GetIncomingLetterByID(ctx context.Context, id uuid.UUID) (*contract.IncomingLetterResponse, error) { entity, err := p.letterRepo.Get(ctx, id) if err != nil { return nil, err } atts, _ := p.attachRepo.ListByLetter(ctx, id) return transformer.LetterEntityToContract(entity, atts), nil } func (p *LetterProcessorImpl) ListIncomingLetters(ctx context.Context, req *contract.ListIncomingLettersRequest) (*contract.ListIncomingLettersResponse, error) { page, limit := req.Page, req.Limit if page <= 0 { page = 1 } if limit <= 0 { limit = 10 } filter := repository.ListIncomingLettersFilter{Status: req.Status, Query: req.Query} list, total, err := p.letterRepo.List(ctx, filter, limit, (page-1)*limit) if err != nil { return nil, err } respList := make([]contract.IncomingLetterResponse, 0, len(list)) for _, e := range list { atts, _ := p.attachRepo.ListByLetter(ctx, e.ID) resp := transformer.LetterEntityToContract(&e, atts) respList = append(respList, *resp) } return &contract.ListIncomingLettersResponse{Letters: respList, Pagination: transformer.CreatePaginationResponse(int(total), page, limit)}, nil } 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) out = transformer.LetterEntityToContract(entity, atts) 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 created := make([]entities.LetterDisposition, 0, len(req.ToDepartmentIDs)) for _, toDept := range req.ToDepartmentIDs { disp := entities.LetterDisposition{ LetterID: req.LetterID, FromDepartmentID: nil, ToDepartmentID: &toDept, Notes: req.Notes, Status: entities.DispositionPending, CreatedBy: userID, } if err := p.dispositionRepo.Create(txCtx, &disp); err != nil { return err } created = append(created, disp) 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" for _, d := range created { ctxMap := map[string]interface{}{"to_department_id": d.ToDepartmentID} if err := p.activity.Log(txCtx, req.LetterID, action, &userID, nil, nil, &d.ID, nil, nil, ctxMap); err != nil { return err } } } } out = &contract.ListDispositionsResponse{Dispositions: transformer.DispositionsToContract(created)} 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) 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{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 := "reference_numberdiscussion.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 }