992 lines
32 KiB
Go
992 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)
|
|
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) {
|
|
// 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
|
|
}
|
|
|
|
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
|
|
}
|
|
} |