package service import ( "context" "eslogad-be/internal/appcontext" "eslogad-be/internal/contract" "eslogad-be/internal/entities" "eslogad-be/internal/processor" "eslogad-be/internal/repository" "github.com/google/uuid" "gorm.io/gorm" ) type LetterOutgoingService interface { CreateOutgoingLetter(ctx context.Context, req *contract.CreateOutgoingLetterRequest) (*contract.OutgoingLetterResponse, error) GetOutgoingLetterByID(ctx context.Context, id uuid.UUID) (*contract.OutgoingLetterResponse, error) ListOutgoingLetters(ctx context.Context, req *contract.ListOutgoingLettersRequest) (*contract.ListOutgoingLettersResponse, error) UpdateOutgoingLetter(ctx context.Context, id uuid.UUID, req *contract.UpdateOutgoingLetterRequest) (*contract.OutgoingLetterResponse, error) DeleteOutgoingLetter(ctx context.Context, id uuid.UUID) error SubmitForApproval(ctx context.Context, letterID uuid.UUID) error ApproveOutgoingLetter(ctx context.Context, letterID uuid.UUID, req *contract.ApproveLetterRequest) error RejectOutgoingLetter(ctx context.Context, letterID uuid.UUID, req *contract.RejectLetterRequest) error SendOutgoingLetter(ctx context.Context, letterID uuid.UUID) error ArchiveOutgoingLetter(ctx context.Context, letterID uuid.UUID) error AddRecipients(ctx context.Context, letterID uuid.UUID, req *contract.AddRecipientsRequest) error UpdateRecipient(ctx context.Context, letterID uuid.UUID, recipientID uuid.UUID, req *contract.UpdateRecipientRequest) error RemoveRecipient(ctx context.Context, letterID uuid.UUID, recipientID uuid.UUID) error AddAttachments(ctx context.Context, letterID uuid.UUID, req *contract.AddAttachmentsRequest) error RemoveAttachment(ctx context.Context, letterID uuid.UUID, attachmentID uuid.UUID) error CreateDiscussion(ctx context.Context, letterID uuid.UUID, req *contract.CreateDiscussionRequest) (*contract.DiscussionResponse, error) UpdateDiscussion(ctx context.Context, discussionID uuid.UUID, req *contract.UpdateDiscussionRequest) error DeleteDiscussion(ctx context.Context, discussionID uuid.UUID) error } type LetterOutgoingServiceImpl struct { processor processor.LetterOutgoingProcessor } func NewLetterOutgoingService(processor processor.LetterOutgoingProcessor) *LetterOutgoingServiceImpl { return &LetterOutgoingServiceImpl{ processor: processor, } } func (s *LetterOutgoingServiceImpl) CreateOutgoingLetter(ctx context.Context, req *contract.CreateOutgoingLetterRequest) (*contract.OutgoingLetterResponse, error) { departmentID := getDepartmentIDFromContext(ctx) letter := &entities.LetterOutgoing{ Subject: req.Subject, Description: req.Description, PriorityID: req.PriorityID, ReceiverInstitutionID: req.ReceiverInstitutionID, IssueDate: req.IssueDate, CreatedBy: req.UserID, } if req.ReferenceNumber != nil { letter.ReferenceNumber = req.ReferenceNumber } var attachments []entities.LetterOutgoingAttachment if len(req.Attachments) > 0 { attachments = make([]entities.LetterOutgoingAttachment, len(req.Attachments)) for i, a := range req.Attachments { attachments[i] = entities.LetterOutgoingAttachment{ FileURL: a.FileURL, FileName: a.FileName, FileType: a.FileType, UploadedBy: &req.UserID, } } } err := s.processor.CreateOutgoingLetter(ctx, letter, attachments, req.UserID, departmentID) if err != nil { return nil, err } result, err := s.processor.GetOutgoingLetterByID(ctx, letter.ID) if err != nil { return nil, err } return transformLetterToResponse(result), nil } func (s *LetterOutgoingServiceImpl) GetOutgoingLetterByID(ctx context.Context, id uuid.UUID) (*contract.OutgoingLetterResponse, error) { letter, err := s.processor.GetOutgoingLetterByID(ctx, id) if err != nil { return nil, err } return transformLetterToResponse(letter), nil } func (s *LetterOutgoingServiceImpl) ListOutgoingLetters(ctx context.Context, req *contract.ListOutgoingLettersRequest) (*contract.ListOutgoingLettersResponse, error) { filter := repository.ListOutgoingLettersFilter{ Status: req.Status, Query: req.Query, CreatedBy: req.CreatedBy, ReceiverInstitutionID: req.ReceiverInstitutionID, FromDate: req.FromDate, ToDate: req.ToDate, } letters, total, err := s.processor.ListOutgoingLetters(ctx, filter, req.Limit, req.Offset) if err != nil { return nil, err } items := make([]*contract.OutgoingLetterResponse, len(letters)) for i, letter := range letters { items[i] = transformLetterToResponse(&letter) } return &contract.ListOutgoingLettersResponse{ Items: items, Total: total, }, nil } func (s *LetterOutgoingServiceImpl) UpdateOutgoingLetter(ctx context.Context, id uuid.UUID, req *contract.UpdateOutgoingLetterRequest) (*contract.OutgoingLetterResponse, error) { userID := getUserIDFromContext(ctx) letter, err := s.processor.GetOutgoingLetterByID(ctx, id) if err != nil { return nil, err } if letter.Status != entities.LetterOutgoingStatusDraft { return nil, gorm.ErrInvalidData } if req.Subject != nil { letter.Subject = *req.Subject } if req.Description != nil { letter.Description = req.Description } if req.PriorityID != nil { letter.PriorityID = req.PriorityID } if req.ReceiverInstitutionID != nil { letter.ReceiverInstitutionID = req.ReceiverInstitutionID } if req.IssueDate != nil { letter.IssueDate = *req.IssueDate } if req.ReferenceNumber != nil { letter.ReferenceNumber = req.ReferenceNumber } err = s.processor.UpdateOutgoingLetter(ctx, letter, userID) if err != nil { return nil, err } result, err := s.processor.GetOutgoingLetterByID(ctx, id) if err != nil { return nil, err } return transformLetterToResponse(result), nil } func (s *LetterOutgoingServiceImpl) DeleteOutgoingLetter(ctx context.Context, id uuid.UUID) error { userID := getUserIDFromContext(ctx) letter, err := s.processor.GetOutgoingLetterByID(ctx, id) if err != nil { return err } if letter.Status != entities.LetterOutgoingStatusDraft { return gorm.ErrInvalidData } return s.processor.DeleteOutgoingLetter(ctx, id, userID) } func (s *LetterOutgoingServiceImpl) SubmitForApproval(ctx context.Context, letterID uuid.UUID) error { userID := getUserIDFromContext(ctx) letter, err := s.processor.GetOutgoingLetterByID(ctx, letterID) if err != nil { return err } if letter.Status != entities.LetterOutgoingStatusDraft { return gorm.ErrInvalidData } if letter.ApprovalFlowID == nil { return gorm.ErrInvalidData } return s.processor.ProcessApprovalSubmission(ctx, letterID, *letter.ApprovalFlowID, userID) } func (s *LetterOutgoingServiceImpl) ApproveOutgoingLetter(ctx context.Context, letterID uuid.UUID, req *contract.ApproveLetterRequest) error { userID := getUserIDFromContext(ctx) letter, err := s.processor.GetOutgoingLetterByID(ctx, letterID) if err != nil { return err } if letter.Status != entities.LetterOutgoingStatusPendingApproval { return gorm.ErrInvalidData } approvals, err := s.processor.GetApprovalsByLetter(ctx, letterID) if err != nil { return err } var currentApproval *entities.LetterOutgoingApproval for i := range approvals { if approvals[i].Status == entities.ApprovalStatusPending { step := approvals[i].Step if (step.ApproverUserID != nil && *step.ApproverUserID == userID) || (step.ApproverRoleID != nil && userHasRole(ctx, *step.ApproverRoleID)) { currentApproval = &approvals[i] break } } } if currentApproval == nil { return gorm.ErrInvalidData } currentApproval.Remarks = req.Remarks allApproved := true for _, approval := range approvals { if approval.ID != currentApproval.ID && approval.Status == entities.ApprovalStatusPending { allApproved = false break } } return s.processor.ProcessApproval(ctx, letterID, currentApproval, userID, allApproved) } func (s *LetterOutgoingServiceImpl) RejectOutgoingLetter(ctx context.Context, letterID uuid.UUID, req *contract.RejectLetterRequest) error { userID := getUserIDFromContext(ctx) letter, err := s.processor.GetOutgoingLetterByID(ctx, letterID) if err != nil { return err } if letter.Status != entities.LetterOutgoingStatusPendingApproval { return gorm.ErrInvalidData } approvals, err := s.processor.GetApprovalsByLetter(ctx, letterID) if err != nil { return err } var currentApproval *entities.LetterOutgoingApproval for i := range approvals { if approvals[i].Status == entities.ApprovalStatusPending { step := approvals[i].Step if (step.ApproverUserID != nil && *step.ApproverUserID == userID) || (step.ApproverRoleID != nil && userHasRole(ctx, *step.ApproverRoleID)) { currentApproval = &approvals[i] break } } } if currentApproval == nil { return gorm.ErrInvalidData } currentApproval.Remarks = &req.Reason return s.processor.ProcessRejection(ctx, letterID, currentApproval, userID) } func (s *LetterOutgoingServiceImpl) SendOutgoingLetter(ctx context.Context, letterID uuid.UUID) error { userID := getUserIDFromContext(ctx) letter, err := s.processor.GetOutgoingLetterByID(ctx, letterID) if err != nil { return err } if letter.Status != entities.LetterOutgoingStatusApproved { return gorm.ErrInvalidData } fromStatus := string(entities.LetterOutgoingStatusApproved) toStatus := string(entities.LetterOutgoingStatusSent) return s.processor.UpdateLetterStatus(ctx, letterID, entities.LetterOutgoingStatusSent, userID, &fromStatus, &toStatus) } func (s *LetterOutgoingServiceImpl) ArchiveOutgoingLetter(ctx context.Context, letterID uuid.UUID) error { userID := getUserIDFromContext(ctx) letter, err := s.processor.GetOutgoingLetterByID(ctx, letterID) if err != nil { return err } if letter.Status != entities.LetterOutgoingStatusSent { return gorm.ErrInvalidData } fromStatus := string(entities.LetterOutgoingStatusSent) toStatus := string(entities.LetterOutgoingStatusArchived) return s.processor.UpdateLetterStatus(ctx, letterID, entities.LetterOutgoingStatusArchived, userID, &fromStatus, &toStatus) } func (s *LetterOutgoingServiceImpl) AddRecipients(ctx context.Context, letterID uuid.UUID, req *contract.AddRecipientsRequest) error { userID := getUserIDFromContext(ctx) letter, err := s.processor.GetOutgoingLetterByID(ctx, letterID) if err != nil { return err } if letter.Status != entities.LetterOutgoingStatusDraft { return gorm.ErrInvalidData } recipients := make([]entities.LetterOutgoingRecipient, len(req.Recipients)) for i, r := range req.Recipients { recipients[i] = entities.LetterOutgoingRecipient{ LetterID: letterID, RecipientName: r.Name, RecipientEmail: r.Email, RecipientPosition: r.Position, RecipientInstitution: r.Institution, IsPrimary: r.IsPrimary, } } return s.processor.AddRecipients(ctx, letterID, recipients, userID) } func (s *LetterOutgoingServiceImpl) UpdateRecipient(ctx context.Context, letterID uuid.UUID, recipientID uuid.UUID, req *contract.UpdateRecipientRequest) error { letter, err := s.processor.GetOutgoingLetterByID(ctx, letterID) if err != nil { return err } if letter.Status != entities.LetterOutgoingStatusDraft { return gorm.ErrInvalidData } recipient := &entities.LetterOutgoingRecipient{ ID: recipientID, RecipientName: req.Name, RecipientEmail: req.Email, RecipientPosition: req.Position, RecipientInstitution: req.Institution, IsPrimary: req.IsPrimary, } return s.processor.UpdateRecipient(ctx, recipient) } func (s *LetterOutgoingServiceImpl) RemoveRecipient(ctx context.Context, letterID uuid.UUID, recipientID uuid.UUID) error { userID := getUserIDFromContext(ctx) letter, err := s.processor.GetOutgoingLetterByID(ctx, letterID) if err != nil { return err } if letter.Status != entities.LetterOutgoingStatusDraft { return gorm.ErrInvalidData } return s.processor.RemoveRecipient(ctx, letterID, recipientID, userID) } func (s *LetterOutgoingServiceImpl) AddAttachments(ctx context.Context, letterID uuid.UUID, req *contract.AddAttachmentsRequest) error { userID := getUserIDFromContext(ctx) letter, err := s.processor.GetOutgoingLetterByID(ctx, letterID) if err != nil { return err } if letter.Status != entities.LetterOutgoingStatusDraft { return gorm.ErrInvalidData } attachments := make([]entities.LetterOutgoingAttachment, len(req.Attachments)) for i, a := range req.Attachments { attachments[i] = entities.LetterOutgoingAttachment{ LetterID: letterID, FileURL: a.FileURL, FileName: a.FileName, FileType: a.FileType, UploadedBy: &userID, } } return s.processor.AddAttachments(ctx, letterID, attachments, userID) } func (s *LetterOutgoingServiceImpl) RemoveAttachment(ctx context.Context, letterID uuid.UUID, attachmentID uuid.UUID) error { userID := getUserIDFromContext(ctx) letter, err := s.processor.GetOutgoingLetterByID(ctx, letterID) if err != nil { return err } if letter.Status != entities.LetterOutgoingStatusDraft { return gorm.ErrInvalidData } return s.processor.RemoveAttachment(ctx, letterID, attachmentID, userID) } func (s *LetterOutgoingServiceImpl) CreateDiscussion(ctx context.Context, letterID uuid.UUID, req *contract.CreateDiscussionRequest) (*contract.DiscussionResponse, error) { userID := getUserIDFromContext(ctx) discussion := &entities.LetterOutgoingDiscussion{ LetterID: letterID, ParentID: req.ParentID, UserID: userID, Message: req.Message, } if req.Mentions != nil { discussion.Mentions = req.Mentions } var attachments []entities.LetterOutgoingDiscussionAttachment if len(req.Attachments) > 0 { attachments = make([]entities.LetterOutgoingDiscussionAttachment, len(req.Attachments)) for i, a := range req.Attachments { attachments[i] = entities.LetterOutgoingDiscussionAttachment{ DiscussionID: discussion.ID, FileURL: a.FileURL, FileName: a.FileName, FileType: a.FileType, UploadedBy: &userID, } } } err := s.processor.CreateDiscussion(ctx, discussion, attachments, userID) if err != nil { return nil, err } result, err := s.processor.GetDiscussionByID(ctx, discussion.ID) if err != nil { return nil, err } return transformDiscussionToResponse(result), nil } func (s *LetterOutgoingServiceImpl) UpdateDiscussion(ctx context.Context, discussionID uuid.UUID, req *contract.UpdateDiscussionRequest) error { discussion, err := s.processor.GetDiscussionByID(ctx, discussionID) if err != nil { return err } userID := getUserIDFromContext(ctx) if discussion.UserID != userID { return gorm.ErrInvalidData } discussion.Message = req.Message if req.Mentions != nil { discussion.Mentions = req.Mentions } return s.processor.UpdateDiscussion(ctx, discussion) } func (s *LetterOutgoingServiceImpl) DeleteDiscussion(ctx context.Context, discussionID uuid.UUID) error { discussion, err := s.processor.GetDiscussionByID(ctx, discussionID) if err != nil { return err } userID := getUserIDFromContext(ctx) if discussion.UserID != userID { return gorm.ErrInvalidData } return s.processor.DeleteDiscussion(ctx, discussionID) } func getUserIDFromContext(ctx context.Context) uuid.UUID { appCtx := appcontext.FromGinContext(ctx) if appCtx != nil { return appCtx.UserID } return uuid.New() } func getDepartmentIDFromContext(ctx context.Context) uuid.UUID { appCtx := appcontext.FromGinContext(ctx) if appCtx != nil { return appCtx.DepartmentID } return uuid.Nil } func userHasRole(ctx context.Context, roleID uuid.UUID) bool { return false } func transformLetterToResponse(letter *entities.LetterOutgoing) *contract.OutgoingLetterResponse { resp := &contract.OutgoingLetterResponse{ ID: letter.ID, LetterNumber: letter.LetterNumber, ReferenceNumber: letter.ReferenceNumber, Subject: letter.Subject, Description: letter.Description, PriorityID: letter.PriorityID, ReceiverInstitutionID: letter.ReceiverInstitutionID, IssueDate: letter.IssueDate, Status: string(letter.Status), ApprovalFlowID: letter.ApprovalFlowID, CreatedBy: letter.CreatedBy, CreatedAt: letter.CreatedAt, UpdatedAt: letter.UpdatedAt, } if letter.Priority != nil { resp.Priority = &contract.PriorityResponse{ ID: letter.Priority.ID.String(), Name: letter.Priority.Name, Level: letter.Priority.Level, CreatedAt: letter.Priority.CreatedAt, UpdatedAt: letter.Priority.UpdatedAt, } } if letter.ReceiverInstitution != nil { resp.ReceiverInstitution = &contract.InstitutionResponse{ ID: letter.ReceiverInstitution.ID.String(), Name: letter.ReceiverInstitution.Name, Type: string(letter.ReceiverInstitution.Type), Address: letter.ReceiverInstitution.Address, ContactPerson: letter.ReceiverInstitution.ContactPerson, Phone: letter.ReceiverInstitution.Phone, Email: letter.ReceiverInstitution.Email, CreatedAt: letter.ReceiverInstitution.CreatedAt, UpdatedAt: letter.ReceiverInstitution.UpdatedAt, } } if len(letter.Recipients) > 0 { resp.Recipients = make([]contract.OutgoingLetterRecipientResponse, len(letter.Recipients)) for i, recipient := range letter.Recipients { resp.Recipients[i] = contract.OutgoingLetterRecipientResponse{ ID: recipient.ID, Name: recipient.RecipientName, Email: recipient.RecipientEmail, Position: recipient.RecipientPosition, Institution: recipient.RecipientInstitution, IsPrimary: recipient.IsPrimary, CreatedAt: recipient.CreatedAt, } } } if len(letter.Attachments) > 0 { resp.Attachments = make([]contract.OutgoingLetterAttachmentResponse, len(letter.Attachments)) for i, attachment := range letter.Attachments { resp.Attachments[i] = contract.OutgoingLetterAttachmentResponse{ ID: attachment.ID, FileURL: attachment.FileURL, FileName: attachment.FileName, FileType: attachment.FileType, UploadedAt: attachment.UploadedAt, } } } if len(letter.Approvals) > 0 { resp.Approvals = make([]contract.OutgoingLetterApprovalResponse, len(letter.Approvals)) for i, approval := range letter.Approvals { approvalResp := contract.OutgoingLetterApprovalResponse{ ID: approval.ID, ApproverID: approval.ApproverID, Status: string(approval.Status), Remarks: approval.Remarks, ActedAt: approval.ActedAt, CreatedAt: approval.CreatedAt, } // Include step order if step is loaded if approval.Step != nil { approvalResp.StepOrder = approval.Step.StepOrder } resp.Approvals[i] = approvalResp } } return resp } func transformDiscussionToResponse(discussion *entities.LetterOutgoingDiscussion) *contract.DiscussionResponse { return &contract.DiscussionResponse{ ID: discussion.ID, UserID: discussion.UserID, Message: discussion.Message, CreatedAt: discussion.CreatedAt, UpdatedAt: discussion.UpdatedAt, } }