package service import ( "context" "time" "eslogad-be/internal/contract" "eslogad-be/internal/entities" "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 { 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 txManager *repository.TxManager } func NewLetterOutgoingService( 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, txManager *repository.TxManager, ) *LetterOutgoingServiceImpl { return &LetterOutgoingServiceImpl{ db: db, letterRepo: letterRepo, attachmentRepo: attachmentRepo, recipientRepo: recipientRepo, discussionRepo: discussionRepo, discussionAttachmentRepo: discussionAttachmentRepo, activityLogRepo: activityLogRepo, approvalFlowRepo: approvalFlowRepo, approvalRepo: approvalRepo, txManager: txManager, } } func (s *LetterOutgoingServiceImpl) CreateOutgoingLetter(ctx context.Context, req *contract.CreateOutgoingLetterRequest) (*contract.OutgoingLetterResponse, error) { letter := &entities.LetterOutgoing{ Subject: req.Subject, Description: req.Description, PriorityID: req.PriorityID, ReceiverInstitutionID: req.ReceiverInstitutionID, IssueDate: req.IssueDate, Status: entities.LetterOutgoingStatusDraft, ApprovalFlowID: req.ApprovalFlowID, CreatedBy: req.UserID, } if req.ReferenceNumber != nil { letter.ReferenceNumber = req.ReferenceNumber } err := s.txManager.WithTransaction(ctx, func(txCtx context.Context) error { if err := s.letterRepo.Create(txCtx, letter); err != nil { return err } if len(req.Recipients) > 0 { recipients := make([]entities.LetterOutgoingRecipient, len(req.Recipients)) for i, r := range req.Recipients { recipients[i] = entities.LetterOutgoingRecipient{ LetterID: letter.ID, RecipientName: r.Name, RecipientEmail: r.Email, RecipientPosition: r.Position, RecipientInstitution: r.Institution, IsPrimary: r.IsPrimary, } } if err := s.recipientRepo.CreateBulk(txCtx, recipients); err != nil { return err } } if len(req.Attachments) > 0 { attachments := make([]entities.LetterOutgoingAttachment, len(req.Attachments)) for i, a := range req.Attachments { attachments[i] = entities.LetterOutgoingAttachment{ LetterID: letter.ID, FileURL: a.FileURL, FileName: a.FileName, FileType: a.FileType, UploadedBy: &req.UserID, } } if err := s.attachmentRepo.CreateBulk(txCtx, attachments); err != nil { return err } } activityLog := &entities.LetterOutgoingActivityLog{ LetterID: letter.ID, ActionType: entities.LetterOutgoingActionCreated, ActorUserID: &req.UserID, } if err := s.activityLogRepo.Create(txCtx, activityLog); err != nil { return err } return nil }) if err != nil { return nil, err } result, err := s.letterRepo.Get(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.letterRepo.Get(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.letterRepo.List(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.letterRepo.Get(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.txManager.WithTransaction(ctx, func(txCtx context.Context) error { if err := s.letterRepo.Update(txCtx, letter); err != nil { return err } activityLog := &entities.LetterOutgoingActivityLog{ LetterID: letter.ID, ActionType: entities.LetterOutgoingActionUpdated, ActorUserID: &userID, } if err := s.activityLogRepo.Create(txCtx, activityLog); err != nil { return err } return nil }) if err != nil { return nil, err } result, err := s.letterRepo.Get(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.letterRepo.Get(ctx, id) if err != nil { return err } if letter.Status != entities.LetterOutgoingStatusDraft { return gorm.ErrInvalidData } return s.txManager.WithTransaction(ctx, func(txCtx context.Context) error { if err := s.letterRepo.SoftDelete(txCtx, id); err != nil { return err } activityLog := &entities.LetterOutgoingActivityLog{ LetterID: letter.ID, ActionType: entities.LetterOutgoingActionDeleted, ActorUserID: &userID, } if err := s.activityLogRepo.Create(txCtx, activityLog); err != nil { return err } return nil }) } func (s *LetterOutgoingServiceImpl) SubmitForApproval(ctx context.Context, letterID uuid.UUID) error { userID := getUserIDFromContext(ctx) letter, err := s.letterRepo.Get(ctx, letterID) if err != nil { return err } if letter.Status != entities.LetterOutgoingStatusDraft { return gorm.ErrInvalidData } if letter.ApprovalFlowID == nil { return gorm.ErrInvalidData } flow, err := s.approvalFlowRepo.Get(ctx, *letter.ApprovalFlowID) if err != nil { return err } return s.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 := s.approvalRepo.CreateBulk(txCtx, approvals); err != nil { return err } if err := s.letterRepo.UpdateStatus(txCtx, letterID, entities.LetterOutgoingStatusPendingApproval); err != nil { return err } activityLog := &entities.LetterOutgoingActivityLog{ LetterID: letterID, ActionType: entities.LetterOutgoingActionSubmittedApproval, ActorUserID: &userID, FromStatus: ptr(string(entities.LetterOutgoingStatusDraft)), ToStatus: ptr(string(entities.LetterOutgoingStatusPendingApproval)), } if err := s.activityLogRepo.Create(txCtx, activityLog); err != nil { return err } return nil }) } func (s *LetterOutgoingServiceImpl) ApproveOutgoingLetter(ctx context.Context, letterID uuid.UUID, req *contract.ApproveLetterRequest) error { userID := getUserIDFromContext(ctx) letter, err := s.letterRepo.Get(ctx, letterID) if err != nil { return err } if letter.Status != entities.LetterOutgoingStatusPendingApproval { return gorm.ErrInvalidData } approvals, err := s.approvalRepo.ListByLetter(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 } return s.txManager.WithTransaction(ctx, func(txCtx context.Context) error { now := time.Now() currentApproval.Status = entities.ApprovalStatusApproved currentApproval.ApproverID = &userID currentApproval.ActedAt = &now currentApproval.Remarks = req.Remarks if err := s.approvalRepo.Update(txCtx, currentApproval); err != nil { return err } allApproved := true for _, approval := range approvals { if approval.ID != currentApproval.ID && approval.Status == entities.ApprovalStatusPending { allApproved = false break } } if allApproved { if err := s.letterRepo.UpdateStatus(txCtx, letterID, entities.LetterOutgoingStatusApproved); err != nil { return err } } activityLog := &entities.LetterOutgoingActivityLog{ LetterID: letterID, ActionType: entities.LetterOutgoingActionApproved, ActorUserID: &userID, TargetID: ¤tApproval.ID, } if err := s.activityLogRepo.Create(txCtx, activityLog); err != nil { return err } return nil }) } func (s *LetterOutgoingServiceImpl) RejectOutgoingLetter(ctx context.Context, letterID uuid.UUID, req *contract.RejectLetterRequest) error { userID := getUserIDFromContext(ctx) letter, err := s.letterRepo.Get(ctx, letterID) if err != nil { return err } if letter.Status != entities.LetterOutgoingStatusPendingApproval { return gorm.ErrInvalidData } approvals, err := s.approvalRepo.ListByLetter(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 } return s.txManager.WithTransaction(ctx, func(txCtx context.Context) error { now := time.Now() currentApproval.Status = entities.ApprovalStatusRejected currentApproval.ApproverID = &userID currentApproval.ActedAt = &now currentApproval.Remarks = &req.Reason if err := s.approvalRepo.Update(txCtx, currentApproval); err != nil { return err } if err := s.letterRepo.UpdateStatus(txCtx, letterID, entities.LetterOutgoingStatusDraft); err != nil { return err } activityLog := &entities.LetterOutgoingActivityLog{ LetterID: letterID, ActionType: entities.LetterOutgoingActionRejected, ActorUserID: &userID, TargetID: ¤tApproval.ID, FromStatus: ptr(string(entities.LetterOutgoingStatusPendingApproval)), ToStatus: ptr(string(entities.LetterOutgoingStatusDraft)), } if err := s.activityLogRepo.Create(txCtx, activityLog); err != nil { return err } return nil }) } func (s *LetterOutgoingServiceImpl) SendOutgoingLetter(ctx context.Context, letterID uuid.UUID) error { userID := getUserIDFromContext(ctx) letter, err := s.letterRepo.Get(ctx, letterID) if err != nil { return err } if letter.Status != entities.LetterOutgoingStatusApproved { return gorm.ErrInvalidData } return s.txManager.WithTransaction(ctx, func(txCtx context.Context) error { if err := s.letterRepo.UpdateStatus(txCtx, letterID, entities.LetterOutgoingStatusSent); err != nil { return err } activityLog := &entities.LetterOutgoingActivityLog{ LetterID: letterID, ActionType: entities.LetterOutgoingActionSent, ActorUserID: &userID, FromStatus: ptr(string(entities.LetterOutgoingStatusApproved)), ToStatus: ptr(string(entities.LetterOutgoingStatusSent)), } if err := s.activityLogRepo.Create(txCtx, activityLog); err != nil { return err } return nil }) } func (s *LetterOutgoingServiceImpl) ArchiveOutgoingLetter(ctx context.Context, letterID uuid.UUID) error { userID := getUserIDFromContext(ctx) letter, err := s.letterRepo.Get(ctx, letterID) if err != nil { return err } if letter.Status != entities.LetterOutgoingStatusSent { return gorm.ErrInvalidData } return s.txManager.WithTransaction(ctx, func(txCtx context.Context) error { if err := s.letterRepo.UpdateStatus(txCtx, letterID, entities.LetterOutgoingStatusArchived); err != nil { return err } activityLog := &entities.LetterOutgoingActivityLog{ LetterID: letterID, ActionType: entities.LetterOutgoingActionArchived, ActorUserID: &userID, FromStatus: ptr(string(entities.LetterOutgoingStatusSent)), ToStatus: ptr(string(entities.LetterOutgoingStatusArchived)), } if err := s.activityLogRepo.Create(txCtx, activityLog); err != nil { return err } return nil }) } func (s *LetterOutgoingServiceImpl) AddRecipients(ctx context.Context, letterID uuid.UUID, req *contract.AddRecipientsRequest) error { userID := getUserIDFromContext(ctx) letter, err := s.letterRepo.Get(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.txManager.WithTransaction(ctx, func(txCtx context.Context) error { if err := s.recipientRepo.CreateBulk(txCtx, recipients); err != nil { return err } activityLog := &entities.LetterOutgoingActivityLog{ LetterID: letterID, ActionType: entities.LetterOutgoingActionRecipientAdded, ActorUserID: &userID, } if err := s.activityLogRepo.Create(txCtx, activityLog); err != nil { return err } return nil }) } func (s *LetterOutgoingServiceImpl) UpdateRecipient(ctx context.Context, letterID uuid.UUID, recipientID uuid.UUID, req *contract.UpdateRecipientRequest) error { letter, err := s.letterRepo.Get(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.recipientRepo.Update(ctx, recipient) } func (s *LetterOutgoingServiceImpl) RemoveRecipient(ctx context.Context, letterID uuid.UUID, recipientID uuid.UUID) error { userID := getUserIDFromContext(ctx) letter, err := s.letterRepo.Get(ctx, letterID) if err != nil { return err } if letter.Status != entities.LetterOutgoingStatusDraft { return gorm.ErrInvalidData } return s.txManager.WithTransaction(ctx, func(txCtx context.Context) error { if err := s.recipientRepo.Delete(txCtx, recipientID); err != nil { return err } activityLog := &entities.LetterOutgoingActivityLog{ LetterID: letterID, ActionType: entities.LetterOutgoingActionRecipientRemoved, ActorUserID: &userID, TargetID: &recipientID, } if err := s.activityLogRepo.Create(txCtx, activityLog); err != nil { return err } return nil }) } func (s *LetterOutgoingServiceImpl) AddAttachments(ctx context.Context, letterID uuid.UUID, req *contract.AddAttachmentsRequest) error { userID := getUserIDFromContext(ctx) letter, err := s.letterRepo.Get(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.txManager.WithTransaction(ctx, func(txCtx context.Context) error { if err := s.attachmentRepo.CreateBulk(txCtx, attachments); err != nil { return err } activityLog := &entities.LetterOutgoingActivityLog{ LetterID: letterID, ActionType: entities.LetterOutgoingActionAttachmentAdded, ActorUserID: &userID, } if err := s.activityLogRepo.Create(txCtx, activityLog); err != nil { return err } return nil }) } func (s *LetterOutgoingServiceImpl) RemoveAttachment(ctx context.Context, letterID uuid.UUID, attachmentID uuid.UUID) error { userID := getUserIDFromContext(ctx) letter, err := s.letterRepo.Get(ctx, letterID) if err != nil { return err } if letter.Status != entities.LetterOutgoingStatusDraft { return gorm.ErrInvalidData } return s.txManager.WithTransaction(ctx, func(txCtx context.Context) error { if err := s.attachmentRepo.Delete(txCtx, attachmentID); err != nil { return err } activityLog := &entities.LetterOutgoingActivityLog{ LetterID: letterID, ActionType: entities.LetterOutgoingActionAttachmentRemoved, ActorUserID: &userID, TargetID: &attachmentID, } if err := s.activityLogRepo.Create(txCtx, activityLog); err != nil { return err } return nil }) } 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 } err := s.txManager.WithTransaction(ctx, func(txCtx context.Context) error { if err := s.discussionRepo.Create(txCtx, discussion); err != nil { return err } 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, } } if err := s.discussionAttachmentRepo.CreateBulk(txCtx, attachments); err != nil { return err } } activityLog := &entities.LetterOutgoingActivityLog{ LetterID: letterID, ActionType: entities.LetterOutgoingActionDiscussionAdded, ActorUserID: &userID, TargetID: &discussion.ID, } if err := s.activityLogRepo.Create(txCtx, activityLog); err != nil { return err } return nil }) if err != nil { return nil, err } result, err := s.discussionRepo.Get(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.discussionRepo.Get(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.discussionRepo.Update(ctx, discussion) } func (s *LetterOutgoingServiceImpl) DeleteDiscussion(ctx context.Context, discussionID uuid.UUID) error { discussion, err := s.discussionRepo.Get(ctx, discussionID) if err != nil { return err } userID := getUserIDFromContext(ctx) if discussion.UserID != userID { return gorm.ErrInvalidData } return s.discussionRepo.Delete(ctx, discussionID) } func getUserIDFromContext(ctx context.Context) uuid.UUID { return uuid.New() } func userHasRole(ctx context.Context, roleID uuid.UUID) bool { return false } func ptr(s string) *string { return &s } 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, } } } // Include Approvals if loaded 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, } }