dukcapil/internal/service/letter_service.go
Aditya Siregar e3cd04a90e add archive
2025-10-13 07:43:18 +07:00

993 lines
32 KiB
Go

package service
import (
"context"
"fmt"
"log"
"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)
SearchIncomingLetters(ctx context.Context, filters map[string]interface{}, page, limit int, sortBy, sortOrder string) ([]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)
ArchiveIncomingLetter(ctx context.Context, letterID uuid.UUID) 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) SearchIncomingLetters(ctx context.Context, req *contract.SearchIncomingLettersRequest) (*contract.SearchIncomingLettersResponse, error) {
appCtx := appcontext.FromGinContext(ctx)
userID := appCtx.UserID
departmentID := appCtx.DepartmentID
// Build search filters
filters := buildIncomingSearchFilters(req, userID, departmentID)
// Execute search with pagination
letters, total, err := s.processor.SearchIncomingLetters(ctx, filters, req.Page, req.Limit, req.SortBy, req.SortOrder)
if err != nil {
return nil, err
}
// Collect IDs for batch loading
letterIDs := make([]uuid.UUID, len(letters))
priorityIDMap := make(map[uuid.UUID]bool)
institutionIDMap := make(map[uuid.UUID]bool)
for i, letter := range letters {
letterIDs[i] = letter.ID
if letter.PriorityID != nil {
priorityIDMap[*letter.PriorityID] = true
}
if letter.SenderInstitutionID != nil {
institutionIDMap[*letter.SenderInstitutionID] = true
}
}
// Convert maps to slices
priorityIDSlice := make([]uuid.UUID, 0, len(priorityIDMap))
for id := range priorityIDMap {
priorityIDSlice = append(priorityIDSlice, id)
}
institutionIDSlice := make([]uuid.UUID, 0, len(institutionIDMap))
for id := range institutionIDMap {
institutionIDSlice = append(institutionIDSlice, id)
}
// Parallel batch loading
type batchLoadResult struct {
attachments map[uuid.UUID][]entities.LetterIncomingAttachment
recipients map[uuid.UUID]*entities.LetterIncomingRecipient
priorities map[uuid.UUID]*entities.Priority
institutions map[uuid.UUID]*entities.Institution
}
var result batchLoadResult
errChan := make(chan error, 4)
// Load attachments
go func() {
result.attachments, err = s.processor.GetBatchAttachments(ctx, letterIDs)
errChan <- err
}()
// Load recipients for user
go func() {
result.recipients, err = s.processor.GetBatchRecipientsByUser(ctx, letterIDs, userID)
errChan <- err
}()
// Load priorities
go func() {
result.priorities, err = s.processor.GetBatchPriorities(ctx, priorityIDSlice)
errChan <- err
}()
// Load institutions
go func() {
result.institutions, err = s.processor.GetBatchInstitutions(ctx, institutionIDSlice)
errChan <- err
}()
// Wait for all goroutines and check for errors
for i := 0; i < 4; i++ {
if err := <-errChan; err != nil {
return nil, err
}
}
// Transform letters with batch loaded data
items := make([]contract.IncomingLetterResponse, len(letters))
for i, letter := range letters {
// Attach batch loaded data
attachmentResponses := []contract.IncomingLetterAttachmentResponse{}
if attachments, ok := result.attachments[letter.ID]; ok {
for _, att := range attachments {
attachmentResponses = append(attachmentResponses, contract.IncomingLetterAttachmentResponse{
ID: att.ID,
FileURL: att.FileURL,
FileName: att.FileName,
FileType: att.FileType,
UploadedAt: att.UploadedAt,
})
}
}
var priorityResp *contract.PriorityResponse
if letter.PriorityID != nil {
if priority, ok := result.priorities[*letter.PriorityID]; ok {
priorityResp = &contract.PriorityResponse{
ID: priority.ID.String(),
Name: priority.Name,
Level: priority.Level,
CreatedAt: priority.CreatedAt,
UpdatedAt: priority.UpdatedAt,
}
}
}
var institutionResp *contract.InstitutionResponse
if letter.SenderInstitutionID != nil {
if institution, ok := result.institutions[*letter.SenderInstitutionID]; ok {
institutionResp = &contract.InstitutionResponse{
ID: institution.ID.String(),
Name: institution.Name,
Type: string(institution.Type),
Address: institution.Address,
ContactPerson: institution.ContactPerson,
Phone: institution.Phone,
Email: institution.Email,
CreatedAt: institution.CreatedAt,
UpdatedAt: institution.UpdatedAt,
}
}
}
isRead := false
if recipient, ok := result.recipients[letter.ID]; ok && recipient.ReadAt != nil {
isRead = true
}
items[i] = contract.IncomingLetterResponse{
ID: letter.ID,
LetterNumber: letter.LetterNumber,
ReferenceNumber: letter.ReferenceNumber,
Subject: letter.Subject,
Description: letter.Description,
Priority: priorityResp,
SenderInstitution: institutionResp,
SenderName: letter.SenderName,
ReceivedDate: letter.ReceivedDate,
DueDate: letter.DueDate,
Status: string(letter.Status),
CreatedBy: letter.CreatedBy,
CreatedAt: letter.CreatedAt,
UpdatedAt: letter.UpdatedAt,
Attachments: attachmentResponses,
IsRead: isRead,
}
}
return &contract.SearchIncomingLettersResponse{
Letters: items,
TotalCount: total,
Page: req.Page,
Limit: req.Limit,
}, nil
}
func buildIncomingSearchFilters(req *contract.SearchIncomingLettersRequest, userID, departmentID uuid.UUID) map[string]interface{} {
filters := make(map[string]interface{})
if req.Query != "" {
filters["query"] = req.Query
}
if req.LetterNumber != "" {
filters["letter_number"] = req.LetterNumber
}
if req.Subject != "" {
filters["subject"] = req.Subject
}
if req.Status != "" {
filters["status"] = req.Status
}
if req.PriorityID != nil {
filters["priority_id"] = *req.PriorityID
}
if req.InstitutionID != nil {
filters["sender_institution_id"] = *req.InstitutionID
}
if req.CreatedBy != nil {
filters["created_by"] = *req.CreatedBy
}
if req.DateFrom != nil {
filters["date_from"] = *req.DateFrom
}
if req.DateTo != nil {
filters["date_to"] = *req.DateTo
}
// Add user/department context filters
filters["user_context"] = map[string]interface{}{
"user_id": userID,
"department_id": departmentID,
}
return filters
}
func (s *LetterServiceImpl) CreateDispositions(ctx context.Context, req *contract.CreateLetterDispositionRequest) (*contract.ListDispositionsResponse, error) {
log.Printf("[DEBUG] CreateDispositions START - LetterID: %s\n", req.LetterID.String())
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 {
// Send notifications to newly created recipients
if len(recipients) > 0 {
go s.sendDispositionNotifications(context.Background(), req.LetterID, recipients)
}
// Send notification to letter creator about new disposition
go s.sendDispositionCreatorNotification(context.Background(), req.LetterID, userID)
}
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 {
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
}
// Send notifications to mentioned users asynchronously
if s.notificationProcessor != nil && req.Mentions != nil {
go s.sendDiscussionMentionNotifications(context.Background(), letterID, userID, req.Mentions, req.Message)
}
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) {
var result *contract.DepartmentDispositionStatusResponse
err := s.txManager.WithTransaction(ctx, func(txCtx context.Context) error {
var err error
result, err = s.processor.UpdateDispositionStatus(txCtx, req)
if err != nil {
return err
}
// Log activity for disposition status update
if s.activityLogger != nil && result != nil {
userID := appcontext.FromGinContext(txCtx).UserID
if err := s.activityLogger.LogLetterDispositionStatusUpdate(txCtx, req.LetterIncomingID, userID, req.Status); err != nil {
// Don't fail the transaction for logging errors
}
}
return nil
})
if err != nil {
return nil, err
}
// Send notification to letter creator asynchronously
if s.notificationProcessor != nil && result != nil {
go s.sendDispositionStatusUpdateNotification(context.Background(), req.LetterIncomingID, req.Status)
}
return result, nil
}
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) {
// Archive the letters themselves
archivedCount, err := s.processor.BulkArchiveIncomingLetters(ctx, letterIDs)
if err != nil {
return nil, err
}
return &contract.BulkArchiveLettersResponse{
Success: true,
Message: "Letters archived successfully",
ArchivedCount: int(archivedCount),
}, nil
}
func (s *LetterServiceImpl) ArchiveIncomingLetter(ctx context.Context, letterID uuid.UUID) error {
return s.processor.ArchiveIncomingLetter(ctx, letterID)
}
func (s *LetterServiceImpl) sendDiscussionMentionNotifications(ctx context.Context, letterID uuid.UUID, senderUserID uuid.UUID, mentions map[string]interface{}, message string) {
// Extract user_ids from mentions
userIDs := s.extractUserIDsFromMentions(mentions)
if len(userIDs) == 0 {
return
}
// Get letter details for notification
letter, err := s.processor.GetIncomingLetterByID(ctx, letterID)
if err != nil {
return
}
// Get sender user name (you might need to implement this)
appContext := appcontext.FromGinContext(ctx)
senderName := appContext.UserName // or get from user service
// Send notification to each mentioned user
for _, mentionedUserID := range userIDs {
// Don't send notification to the sender themselves
if mentionedUserID == senderUserID {
continue
}
subject := "Anda Disebutkan dalam Diskusi"
notificationMessage := fmt.Sprintf("%s menyebutkan Anda dalam diskusi surat: %s", senderName, letter.Subject)
err := s.notificationProcessor.SendIncomingLetterNotification(
ctx,
letterID,
mentionedUserID,
subject,
notificationMessage)
if err != nil {
// Log error but continue with other notifications
}
}
}
func (s *LetterServiceImpl) extractUserIDsFromMentions(mentions map[string]interface{}) []uuid.UUID {
userIDs := make([]uuid.UUID, 0)
if userIDsInterface, exists := mentions["user_ids"]; exists {
switch userIDsValue := userIDsInterface.(type) {
case []interface{}:
for _, userIDInterface := range userIDsValue {
if userIDStr, ok := userIDInterface.(string); ok {
if userID, err := uuid.Parse(userIDStr); err == nil {
userIDs = append(userIDs, userID)
}
}
}
case []string:
for _, userIDStr := range userIDsValue {
if userID, err := uuid.Parse(userIDStr); err == nil {
userIDs = append(userIDs, userID)
}
}
}
}
return userIDs
}
func (s *LetterServiceImpl) sendDispositionCreatorNotification(ctx context.Context, letterID uuid.UUID, dispositionCreatorID uuid.UUID) {
// Get letter details
letter, err := s.processor.GetIncomingLetterByID(ctx, letterID)
if err != nil {
return
}
fmt.Printf("[DEBUG] Starting sendDispositionCreatorNotification for letterID: %s\n", letterID.String())
fmt.Printf("[DEBUG] Successfully retrieved letter: %s\n", letter.Subject)
fmt.Printf("[DEBUG] Successfully retrieved letter: %s\n", letter.CreatedBy)
letterCreatorID := letter.CreatedBy
// Don't send notification if the disposition creator is the same as letter creator
if letterCreatorID == dispositionCreatorID {
return
}
// Get disposition creator name from context
appContext := appcontext.FromGinContext(ctx)
dispositionCreatorName := appContext.UserName
subject := "Disposisi Baru pada Surat Anda"
message := fmt.Sprintf("Surat yang Anda buat telah didisposisikan %s: %s",
dispositionCreatorName, letter.Subject)
err = s.notificationProcessor.SendIncomingLetterNotification(
ctx,
letterID,
letterCreatorID,
subject,
message)
if err != nil {
// Log error but don't fail the operation
// You might want to add proper logging here
}
}
func (s *LetterServiceImpl) sendDispositionStatusUpdateNotification(ctx context.Context, letterID uuid.UUID, newStatus string) {
// Get letter details
letter, err := s.processor.GetIncomingLetterByID(ctx, letterID)
if err != nil {
// Log error but don't fail
return
}
// Get current user context (the one updating the status)
appContext := appcontext.FromGinContext(ctx)
updaterUserID := appContext.UserID
updaterName := appContext.UserName
letterCreatorID := letter.CreatedBy
// Don't send notification if the updater is the same as letter creator
if letterCreatorID == updaterUserID {
return
}
// Create status-specific notification message
var statusMessage string
switch newStatus {
case "pending":
statusMessage = "sedang menunggu"
case "in_progress":
statusMessage = "sedang diproses"
case "completed":
statusMessage = "telah diselesaikan"
case "cancelled":
statusMessage = "dibatalkan"
default:
statusMessage = fmt.Sprintf("diubah statusnya menjadi %s", newStatus)
}
subject := "Status Disposisi Surat Diperbarui"
message := fmt.Sprintf("Disposisi surat '%s' %s %s",
letter.Subject, statusMessage, updaterName)
err = s.notificationProcessor.SendIncomingLetterNotification(
ctx,
letterID,
letterCreatorID,
subject,
message)
if err != nil {
// Log error but don't fail the operation
// You might want to add proper logging here
}
}