dukcapil/internal/service/letter_service.go
2025-09-09 14:41:00 +07:00

593 lines
20 KiB
Go

package service
import (
"context"
"fmt"
"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)
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)
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)
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
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),
}
errChan := make(chan error, 4)
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
}()
for i := 0; i < 4; 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]
}
isRead := false
if recipient, exists := batchData.recipients[letter.ID]; exists && recipient != nil {
isRead = recipient.ReadAt != nil
}
resp := transformer.LetterEntityToContract(&letter, attachments, 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) CreateDispositions(ctx context.Context, req *contract.CreateLetterDispositionRequest) (*contract.ListDispositionsResponse, error) {
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 && len(recipients) > 0 {
go s.sendDispositionNotifications(context.Background(), req.LetterID, recipients)
}
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 {
// Create a simple activity log
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
}
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) {
// For now, delegate to the processor which handles this
// The processor needs to be refactored to remove context extraction
return s.processor.UpdateDispositionStatus(ctx, req)
}
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) {
// Extract user context to archive only for the current user
appCtx := appcontext.FromGinContext(ctx)
userID := appCtx.UserID
// Archive letters only for the current user
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
}