Init All Docs

This commit is contained in:
Aditya Siregar
2025-09-08 12:24:37 +07:00
parent aa662a321f
commit 2319019eb2
68 changed files with 5417 additions and 437 deletions
@@ -35,3 +35,28 @@ func (p *ActivityLogProcessorImpl) Log(ctx context.Context, letterID uuid.UUID,
}
return p.repo.Create(ctx, entry)
}
func (p *ActivityLogProcessorImpl) LogLetterDispositionStatusUpdate(ctx context.Context, letterID uuid.UUID, userID uuid.UUID, status string) error {
return p.Log(ctx, letterID, "disposition_status_update", &userID, nil, nil, nil, nil, &status, map[string]interface{}{
"status": status,
})
}
func (p *ActivityLogProcessorImpl) LogLetterCreated(ctx context.Context, letterID uuid.UUID, userID uuid.UUID, letterNumber string) error {
return p.Log(ctx, letterID, "letter.created", &userID, nil, nil, nil, nil, nil, map[string]interface{}{
"letter_number": letterNumber,
})
}
func (p *ActivityLogProcessorImpl) LogAttachmentUploaded(ctx context.Context, letterID uuid.UUID, userID uuid.UUID, fileName string, fileType string) error {
return p.Log(ctx, letterID, "attachment.uploaded", &userID, nil, nil, nil, nil, nil, map[string]interface{}{
"file_name": fileName,
"file_type": fileType,
})
}
func (p *ActivityLogProcessorImpl) LogDispositionCreated(ctx context.Context, letterID uuid.UUID, userID uuid.UUID, departmentCount int) error {
return p.Log(ctx, letterID, "disposition.created", &userID, nil, nil, nil, nil, nil, map[string]interface{}{
"department_count": departmentCount,
})
}
+112
View File
@@ -0,0 +1,112 @@
package processor
import (
"context"
"sync"
"time"
"eslogad-be/internal/contract"
"eslogad-be/internal/repository"
"eslogad-be/internal/transformer"
"github.com/google/uuid"
)
// CachedUserProcessor wraps UserProcessor with caching for frequently accessed users
type CachedUserProcessor struct {
userRepo *repository.UserRepositoryImpl
profileRepo *repository.UserProfileRepository
cache map[uuid.UUID]*cacheEntry
mu sync.RWMutex
ttl time.Duration
}
type cacheEntry struct {
user *contract.UserResponse
expiresAt time.Time
}
func NewCachedUserProcessor(userRepo *repository.UserRepositoryImpl, profileRepo *repository.UserProfileRepository) *CachedUserProcessor {
return &CachedUserProcessor{
userRepo: userRepo,
profileRepo: profileRepo,
cache: make(map[uuid.UUID]*cacheEntry),
ttl: 5 * time.Minute, // Cache for 5 minutes
}
}
func (p *CachedUserProcessor) GetUserByIDCached(ctx context.Context, id uuid.UUID) (*contract.UserResponse, error) {
p.mu.RLock()
if entry, exists := p.cache[id]; exists {
if entry.expiresAt.After(time.Now()) {
p.mu.RUnlock()
return entry.user, nil
}
}
p.mu.RUnlock()
// Not in cache or expired, fetch from database using the light method
user, err := p.userRepo.GetByIDLight(ctx, id)
if err != nil {
return nil, err
}
// Convert to contract response
resp := &contract.UserResponse{
ID: user.ID,
Email: user.Email,
Name: user.Name,
IsActive: user.IsActive,
CreatedAt: user.CreatedAt,
UpdatedAt: user.UpdatedAt,
}
// Store in cache
p.mu.Lock()
p.cache[id] = &cacheEntry{
user: resp,
expiresAt: time.Now().Add(p.ttl),
}
p.mu.Unlock()
// Clean expired entries periodically
go p.cleanExpiredEntries()
return resp, nil
}
// GetUserByIDFull retrieves full user with all relationships - no caching
func (p *CachedUserProcessor) GetUserByIDFull(ctx context.Context, id uuid.UUID) (*contract.UserResponse, error) {
user, err := p.userRepo.GetByID(ctx, id)
if err != nil {
return nil, err
}
resp := transformer.EntityToContract(user)
if resp != nil {
if roles, err := p.userRepo.GetRolesByUserID(ctx, resp.ID); err == nil {
resp.Roles = transformer.RolesToContract(roles)
}
}
return resp, nil
}
// InvalidateCache removes a user from cache
func (p *CachedUserProcessor) InvalidateCache(userID uuid.UUID) {
p.mu.Lock()
delete(p.cache, userID)
p.mu.Unlock()
}
// cleanExpiredEntries removes expired cache entries
func (p *CachedUserProcessor) cleanExpiredEntries() {
p.mu.Lock()
defer p.mu.Unlock()
now := time.Now()
for id, entry := range p.cache {
if entry.expiresAt.Before(now) {
delete(p.cache, id)
}
}
}
+33
View File
@@ -0,0 +1,33 @@
package processor
import (
"context"
"eslogad-be/internal/contract"
"github.com/google/uuid"
)
// CachedUserWrapper wraps CachedUserProcessor to implement middleware.UserProcessor interface
type CachedUserWrapper struct {
cached *CachedUserProcessor
full *UserProcessorImpl
}
// NewCachedUserWrapper creates a new wrapper
func NewCachedUserWrapper(cached *CachedUserProcessor, full *UserProcessorImpl) *CachedUserWrapper {
return &CachedUserWrapper{
cached: cached,
full: full,
}
}
// GetUserByID uses cached version for fast lookups
func (w *CachedUserWrapper) GetUserByID(ctx context.Context, id uuid.UUID) (*contract.UserResponse, error) {
return w.cached.GetUserByIDCached(ctx, id)
}
// GetUserByIDFull uses full version when all data is needed
func (w *CachedUserWrapper) GetUserByIDFull(ctx context.Context, id uuid.UUID) (*contract.UserResponse, error) {
return w.full.GetUserByID(ctx, id)
}
@@ -0,0 +1,180 @@
package processor
import (
"context"
"time"
"eslogad-be/internal/contract"
"eslogad-be/internal/entities"
"eslogad-be/internal/repository"
"eslogad-be/internal/transformer"
"github.com/google/uuid"
)
type LetterDispositionDepartmentProcessor interface {
GetByLetterIncomingID(ctx context.Context, letterIncomingID uuid.UUID) ([]entities.LetterIncomingDispositionDepartment, error)
GetDepartmentDispositionStatus(ctx context.Context, letterIncomingID uuid.UUID) (*contract.ListDepartmentDispositionStatusResponse, error)
UpdateDispositionStatus(ctx context.Context, letterIncomingID uuid.UUID, departmentID uuid.UUID, userID uuid.UUID, req *contract.UpdateDispositionStatusRequest) (*contract.DepartmentDispositionStatusResponse, error)
CheckAndUpdateLetterCompletionStatus(ctx context.Context, letterIncomingID uuid.UUID) error
}
type LetterDispositionDepartmentProcessorImpl struct {
dispositionDeptRepo *repository.LetterIncomingDispositionDepartmentRepository
dispositionNoteRepo *repository.DispositionNoteRepository
letterRepo *repository.LetterIncomingRepository
}
func NewLetterDispositionDepartmentProcessor(
dispositionDeptRepo *repository.LetterIncomingDispositionDepartmentRepository,
dispositionNoteRepo *repository.DispositionNoteRepository,
letterRepo *repository.LetterIncomingRepository,
) *LetterDispositionDepartmentProcessorImpl {
return &LetterDispositionDepartmentProcessorImpl{
dispositionDeptRepo: dispositionDeptRepo,
dispositionNoteRepo: dispositionNoteRepo,
letterRepo: letterRepo,
}
}
// GetByLetterIncomingID retrieves all disposition departments for a letter
func (p *LetterDispositionDepartmentProcessorImpl) GetByLetterIncomingID(ctx context.Context, letterIncomingID uuid.UUID) ([]entities.LetterIncomingDispositionDepartment, error) {
return p.dispositionDeptRepo.GetByLetterIncomingID(ctx, letterIncomingID)
}
// GetDepartmentDispositionStatus retrieves disposition status for a specific letter
func (p *LetterDispositionDepartmentProcessorImpl) GetDepartmentDispositionStatus(ctx context.Context, letterIncomingID uuid.UUID) (*contract.ListDepartmentDispositionStatusResponse, error) {
dispositions, err := p.dispositionDeptRepo.GetByLetterIncomingID(ctx, letterIncomingID)
if err != nil {
return nil, err
}
response := p.buildDispositionStatusResponse(dispositions)
return response, nil
}
func (p *LetterDispositionDepartmentProcessorImpl) UpdateDispositionStatus(ctx context.Context, letterIncomingID uuid.UUID, departmentID uuid.UUID, userID uuid.UUID, req *contract.UpdateDispositionStatusRequest) (*contract.DepartmentDispositionStatusResponse, error) {
dispDept, err := p.dispositionDeptRepo.GetByDispositionAndDepartment(ctx, letterIncomingID, departmentID)
if err != nil {
return nil, err
}
now := time.Now()
var dispositionStatus entities.LetterIncomingDispositionDepartmentStatus
var readAt, completedAt *time.Time
switch req.Status {
case "completed":
dispositionStatus = entities.DispositionDepartmentStatusCompleted
completedAt = &now
readAt = &now // Mark as read when completing
case "read":
dispositionStatus = entities.DispositionDepartmentStatusRead
readAt = &now
case "dispositioned":
dispositionStatus = entities.DispositionDepartmentStatusDispositioned
default:
dispositionStatus = entities.DispositionDepartmentStatusPending
}
// Extract notes for the update
notes := ""
if req.Notes != nil && *req.Notes != "" {
notes = *req.Notes
}
if err := p.dispositionDeptRepo.UpdateStatus(ctx, dispDept.ID, dispositionStatus, notes, readAt, completedAt); err != nil {
return nil, err
}
// Check and update letter completion status
if err := p.CheckAndUpdateLetterCompletionStatus(ctx, letterIncomingID); err != nil {
// Log error but don't fail the status update
}
// Get updated record for response
updatedDispDept, err := p.dispositionDeptRepo.GetByID(ctx, dispDept.ID)
if err != nil {
return nil, err
}
return p.buildSingleDispositionStatusResponse(updatedDispDept), nil
}
// CheckAndUpdateLetterCompletionStatus checks if all dispositions are completed and updates letter status
func (p *LetterDispositionDepartmentProcessorImpl) CheckAndUpdateLetterCompletionStatus(ctx context.Context, letterIncomingID uuid.UUID) error {
// Get all disposition departments for this letter
dispositions, err := p.dispositionDeptRepo.GetByLetterIncomingID(ctx, letterIncomingID)
if err != nil {
return err
}
// Check if all dispositions are completed
allCompleted := true
for _, disp := range dispositions {
if disp.Status == entities.DispositionDepartmentStatusPending {
allCompleted = false
break
}
}
// If all dispositions are completed, update the letter status to completed
if allCompleted && len(dispositions) > 0 {
letter, err := p.letterRepo.GetByID(ctx, letterIncomingID)
if err != nil {
return err
}
letter.Status = "completed"
if err := p.letterRepo.Update(ctx, letter); err != nil {
return err
}
}
return nil
}
// Helper methods
func (p *LetterDispositionDepartmentProcessorImpl) buildDispositionStatusResponse(dispositions []entities.LetterIncomingDispositionDepartment) *contract.ListDepartmentDispositionStatusResponse {
var response []contract.DepartmentDispositionStatusResponse
for _, disp := range dispositions {
response = append(response, *p.buildSingleDispositionStatusResponse(&disp))
}
return &contract.ListDepartmentDispositionStatusResponse{
Dispositions: response,
Pagination: contract.PaginationResponse{
TotalCount: len(response),
Page: 1,
Limit: len(response),
TotalPages: 1,
},
}
}
func (p *LetterDispositionDepartmentProcessorImpl) buildSingleDispositionStatusResponse(dispDept *entities.LetterIncomingDispositionDepartment) *contract.DepartmentDispositionStatusResponse {
letterResp := transformer.LetterIncomingEntityToContract(dispDept.LetterIncoming)
var fromDept *contract.DepartmentResponse
if dispDept.LetterIncomingDisposition != nil && dispDept.LetterIncomingDisposition.DepartmentID != nil {
fromDept = transformer.DepartmentEntityToContract(&dispDept.LetterIncomingDisposition.Department)
}
return &contract.DepartmentDispositionStatusResponse{
ID: dispDept.ID,
LetterID: dispDept.LetterIncomingID,
Letter: letterResp,
FromDepartmentID: dispDept.LetterIncomingDisposition.DepartmentID,
FromDepartment: fromDept,
ToDepartmentID: dispDept.DepartmentID,
ToDepartment: transformer.DepartmentEntityToContract(dispDept.Department),
Status: string(dispDept.Status),
Notes: dispDept.LetterIncomingDisposition.Notes,
ReadAt: dispDept.ReadAt,
CompletedAt: dispDept.CompletedAt,
CreatedAt: dispDept.CreatedAt,
UpdatedAt: dispDept.UpdatedAt,
}
}
@@ -0,0 +1,240 @@
package processor
import (
"context"
"eslogad-be/internal/contract"
"eslogad-be/internal/entities"
"eslogad-be/internal/repository"
"eslogad-be/internal/transformer"
"github.com/google/uuid"
)
type LetterDispositionProcessor interface {
CreateDispositions(ctx context.Context, req *contract.CreateLetterDispositionRequest) (*contract.ListDispositionsResponse, error)
GetByLetterID(ctx context.Context, letterID uuid.UUID) ([]entities.LetterIncomingDisposition, error)
GetEnhancedDispositionsByLetter(ctx context.Context, letterID uuid.UUID) (*contract.ListEnhancedDispositionsResponse, error)
}
type LetterDispositionProcessorImpl struct {
dispositionRepo *repository.LetterIncomingDispositionRepository
dispositionDeptRepo *repository.LetterIncomingDispositionDepartmentRepository
dispositionActionSelRepo *repository.LetterDispositionActionSelectionRepository
dispositionNoteRepo *repository.DispositionNoteRepository
discussionRepo *repository.LetterDiscussionRepository
dispActionRepo *repository.DispositionActionRepository
activity *ActivityLogProcessorImpl
}
func NewLetterDispositionProcessor(
dispositionRepo *repository.LetterIncomingDispositionRepository,
dispositionDeptRepo *repository.LetterIncomingDispositionDepartmentRepository,
dispositionActionSelRepo *repository.LetterDispositionActionSelectionRepository,
dispositionNoteRepo *repository.DispositionNoteRepository,
discussionRepo *repository.LetterDiscussionRepository,
dispActionRepo *repository.DispositionActionRepository,
activity *ActivityLogProcessorImpl,
) *LetterDispositionProcessorImpl {
return &LetterDispositionProcessorImpl{
dispositionRepo: dispositionRepo,
dispositionDeptRepo: dispositionDeptRepo,
dispositionActionSelRepo: dispositionActionSelRepo,
dispositionNoteRepo: dispositionNoteRepo,
discussionRepo: discussionRepo,
dispActionRepo: dispActionRepo,
activity: activity,
}
}
func (p *LetterDispositionProcessorImpl) CreateDispositions(ctx context.Context, req *contract.CreateLetterDispositionRequest) (*contract.ListDispositionsResponse, error) {
disposition := &entities.LetterIncomingDisposition{
LetterID: req.LetterID,
DepartmentID: &req.FromDepartment,
Notes: req.Notes,
CreatedBy: req.CreatedBy,
}
if err := p.dispositionRepo.Create(ctx, disposition); err != nil {
return nil, err
}
if err := p.createDispositionDepartments(ctx, disposition.ID, req.LetterID, req.ToDepartmentIDs); err != nil {
return nil, err
}
if len(req.SelectedActions) > 0 {
if err := p.createActionSelectionsFromRequest(ctx, disposition.ID, req.SelectedActions); err != nil {
return nil, err
}
}
if p.activity != nil {
p.activity.LogDispositionCreated(ctx, req.LetterID, req.CreatedBy, len(req.ToDepartmentIDs))
}
// Build response
dispositions := []entities.LetterIncomingDisposition{*disposition}
response := p.buildDispositionsResponse(dispositions)
return response, nil
}
func (p *LetterDispositionProcessorImpl) GetByLetterID(ctx context.Context, letterID uuid.UUID) ([]entities.LetterIncomingDisposition, error) {
return p.dispositionRepo.ListByLetter(ctx, letterID)
}
func (p *LetterDispositionProcessorImpl) GetEnhancedDispositionsByLetter(ctx context.Context, letterID uuid.UUID) (*contract.ListEnhancedDispositionsResponse, error) {
// Get dispositions
dispositions, err := p.dispositionRepo.ListByLetter(ctx, letterID)
if err != nil {
return nil, err
}
// Get discussions
discussions, err := p.discussionRepo.ListByLetter(ctx, letterID)
if err != nil {
return nil, err
}
// Build enhanced response
enhancedDispositions := make([]contract.EnhancedDispositionResponse, 0, len(dispositions))
for _, disp := range dispositions {
// Build disposition response using existing structure
var dept contract.DepartmentResponse
if disp.Department.ID != uuid.Nil {
dept = *transformer.DepartmentEntityToContract(&disp.Department)
}
// Build departments
departments := make([]contract.DispositionDepartmentResponse, 0, len(disp.Departments))
for _, d := range disp.Departments {
departments = append(departments, contract.DispositionDepartmentResponse{
ID: d.ID,
DepartmentID: d.DepartmentID,
Department: transformer.DepartmentEntityToContract(d.Department),
})
}
// Build actions
actions := make([]contract.DispositionActionSelectionResponse, 0, len(disp.ActionSelections))
for _, a := range disp.ActionSelections {
if a.Action != nil {
actions = append(actions, contract.DispositionActionSelectionResponse{
ID: a.ID,
ActionID: a.ActionID,
Action: &contract.DispositionActionResponse{
ID: a.Action.ID.String(),
Code: a.Action.Code,
Label: a.Action.Label,
Description: a.Action.Description,
RequiresNote: a.Action.RequiresNote,
},
})
}
}
// Build notes
notes := make([]contract.DispositionNoteResponse, 0, len(disp.DispositionNotes))
for _, n := range disp.DispositionNotes {
var userResp *contract.UserResponse
if n.User != nil {
userResp = transformer.EntityToContract(n.User)
}
notes = append(notes, contract.DispositionNoteResponse{
ID: n.ID,
Note: n.Note,
CreatedAt: n.CreatedAt,
User: userResp,
})
}
enhancedDispositions = append(enhancedDispositions, contract.EnhancedDispositionResponse{
ID: disp.ID,
LetterID: disp.LetterID,
DepartmentID: disp.DepartmentID,
Notes: disp.Notes,
ReadAt: disp.ReadAt,
CreatedBy: disp.CreatedBy,
CreatedAt: disp.CreatedAt,
UpdatedAt: disp.UpdatedAt,
Department: dept,
Departments: departments,
Actions: actions,
DispositionNotes: notes,
})
}
// Get general discussions
var generalDiscussions []contract.LetterDiscussionResponse
for _, disc := range discussions {
generalDiscussions = append(generalDiscussions, *transformer.DiscussionEntityToContract(&disc))
}
return &contract.ListEnhancedDispositionsResponse{
Dispositions: enhancedDispositions,
Discussions: generalDiscussions,
}, nil
}
// Helper methods
func (p *LetterDispositionProcessorImpl) createDispositionDepartments(ctx context.Context, dispositionID, letterID uuid.UUID, departmentIDs []uuid.UUID) error {
departments := make([]entities.LetterIncomingDispositionDepartment, 0, len(departmentIDs))
for _, deptID := range departmentIDs {
departments = append(departments, entities.LetterIncomingDispositionDepartment{
LetterIncomingDispositionID: dispositionID,
LetterIncomingID: letterID,
DepartmentID: deptID,
Status: entities.DispositionDepartmentStatusPending,
})
}
if len(departments) > 0 {
return p.dispositionDeptRepo.CreateBulk(ctx, departments)
}
return nil
}
func (p *LetterDispositionProcessorImpl) createActionSelectionsFromRequest(ctx context.Context, dispositionID uuid.UUID, selectedActions []contract.CreateDispositionActionSelection) error {
selections := make([]entities.LetterDispositionActionSelection, 0, len(selectedActions))
for _, action := range selectedActions {
selections = append(selections, entities.LetterDispositionActionSelection{
DispositionID: dispositionID,
ActionID: action.ActionID,
})
}
if len(selections) > 0 {
return p.dispositionActionSelRepo.CreateBulk(ctx, selections)
}
return nil
}
func (p *LetterDispositionProcessorImpl) buildDispositionsResponse(dispositions []entities.LetterIncomingDisposition) *contract.ListDispositionsResponse {
dispositionResponses := make([]contract.DispositionResponse, 0, len(dispositions))
for _, disp := range dispositions {
dispositionResponses = append(dispositionResponses, *p.buildDispositionResponse(&disp))
}
return &contract.ListDispositionsResponse{
Dispositions: dispositionResponses,
}
}
func (p *LetterDispositionProcessorImpl) buildDispositionResponse(disp *entities.LetterIncomingDisposition) *contract.DispositionResponse {
return &contract.DispositionResponse{
ID: disp.ID,
LetterID: disp.LetterID,
DepartmentID: disp.DepartmentID,
Notes: disp.Notes,
ReadAt: disp.ReadAt,
CreatedBy: disp.CreatedBy,
CreatedAt: disp.CreatedAt,
UpdatedAt: disp.UpdatedAt,
}
}
@@ -43,6 +43,13 @@ type LetterOutgoingProcessor interface {
// GetOutgoingLetterWithDetails fetches letter with all related data
GetOutgoingLetterWithDetails(ctx context.Context, letterID uuid.UUID) (*entities.LetterOutgoing, error)
GetUsersByIDs(ctx context.Context, userIDs []uuid.UUID) ([]entities.User, error)
BulkArchiveOutgoingLetters(ctx context.Context, letterIDs []uuid.UUID) (int64, error)
// Batch loading methods for efficient querying
GetBatchAttachments(ctx context.Context, letterIDs []uuid.UUID) (map[uuid.UUID][]entities.LetterOutgoingAttachment, error)
GetBatchRecipients(ctx context.Context, letterIDs []uuid.UUID) (map[uuid.UUID][]entities.LetterOutgoingRecipient, 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)
}
type LetterOutgoingProcessorImpl struct {
@@ -57,6 +64,8 @@ type LetterOutgoingProcessorImpl struct {
approvalRepo *repository.LetterOutgoingApprovalRepository
numberGenerator *LetterNumberGeneratorImpl
txManager *repository.TxManager
priorityRepo *repository.PriorityRepository
institutionRepo *repository.InstitutionRepository
}
func NewLetterOutgoingProcessor(
@@ -71,6 +80,8 @@ func NewLetterOutgoingProcessor(
approvalRepo *repository.LetterOutgoingApprovalRepository,
numberGenerator *LetterNumberGeneratorImpl,
txManager *repository.TxManager,
priorityRepo *repository.PriorityRepository,
institutionRepo *repository.InstitutionRepository,
) *LetterOutgoingProcessorImpl {
return &LetterOutgoingProcessorImpl{
db: db,
@@ -84,6 +95,8 @@ func NewLetterOutgoingProcessor(
approvalRepo: approvalRepo,
numberGenerator: numberGenerator,
txManager: txManager,
priorityRepo: priorityRepo,
institutionRepo: institutionRepo,
}
}
@@ -766,3 +779,39 @@ func (p *LetterOutgoingProcessorImpl) GetUsersByIDs(ctx context.Context, userIDs
return users, nil
}
func (p *LetterOutgoingProcessorImpl) BulkArchiveOutgoingLetters(ctx context.Context, letterIDs []uuid.UUID) (int64, error) {
return p.letterRepo.BulkArchive(ctx, letterIDs)
}
// GetBatchAttachments fetches attachments for multiple letters in a single query
func (p *LetterOutgoingProcessorImpl) GetBatchAttachments(ctx context.Context, letterIDs []uuid.UUID) (map[uuid.UUID][]entities.LetterOutgoingAttachment, error) {
if p.attachmentRepo == nil || len(letterIDs) == 0 {
return make(map[uuid.UUID][]entities.LetterOutgoingAttachment), nil
}
return p.attachmentRepo.ListByLetterIDs(ctx, letterIDs)
}
// GetBatchRecipients fetches recipients for multiple letters in a single query
func (p *LetterOutgoingProcessorImpl) GetBatchRecipients(ctx context.Context, letterIDs []uuid.UUID) (map[uuid.UUID][]entities.LetterOutgoingRecipient, error) {
if p.recipientRepo == nil || len(letterIDs) == 0 {
return make(map[uuid.UUID][]entities.LetterOutgoingRecipient), nil
}
return p.recipientRepo.ListByLetterIDs(ctx, letterIDs)
}
// GetBatchPriorities fetches priorities by IDs in a single query
func (p *LetterOutgoingProcessorImpl) GetBatchPriorities(ctx context.Context, priorityIDs []uuid.UUID) (map[uuid.UUID]*entities.Priority, error) {
if p.priorityRepo == nil || len(priorityIDs) == 0 {
return make(map[uuid.UUID]*entities.Priority), nil
}
return p.priorityRepo.GetByIDs(ctx, priorityIDs)
}
// GetBatchInstitutions fetches institutions by IDs in a single query
func (p *LetterOutgoingProcessorImpl) GetBatchInstitutions(ctx context.Context, institutionIDs []uuid.UUID) (map[uuid.UUID]*entities.Institution, error) {
if p.institutionRepo == nil || len(institutionIDs) == 0 {
return make(map[uuid.UUID]*entities.Institution), nil
}
return p.institutionRepo.GetByIDs(ctx, institutionIDs)
}
+229 -151
View File
@@ -25,141 +25,68 @@ type LetterProcessorImpl struct {
discussionRepo *repository.LetterDiscussionRepository
settingRepo *repository.AppSettingRepository
recipientRepo *repository.LetterIncomingRecipientRepository
outgoingRecipientRepo *repository.LetterOutgoingRecipientRepository
departmentRepo *repository.DepartmentRepository
userDeptRepo *repository.UserDepartmentRepository
priorityRepo *repository.PriorityRepository
institutionRepo *repository.InstitutionRepository
dispActionRepo *repository.DispositionActionRepository
dispoRoutes *repository.DispositionRouteRepository
numberGenerator *LetterNumberGeneratorImpl
}
func NewLetterProcessor(letterRepo *repository.LetterIncomingRepository, attachRepo *repository.LetterIncomingAttachmentRepository, txManager *repository.TxManager, activity *ActivityLogProcessorImpl, dispRepo *repository.LetterIncomingDispositionRepository, dispDeptRepo *repository.LetterIncomingDispositionDepartmentRepository, dispSelRepo *repository.LetterDispositionActionSelectionRepository, noteRepo *repository.DispositionNoteRepository, discussionRepo *repository.LetterDiscussionRepository, settingRepo *repository.AppSettingRepository, recipientRepo *repository.LetterIncomingRecipientRepository, departmentRepo *repository.DepartmentRepository, userDeptRepo *repository.UserDepartmentRepository, priorityRepo *repository.PriorityRepository, institutionRepo *repository.InstitutionRepository, dispActionRepo *repository.DispositionActionRepository, numberGenerator *LetterNumberGeneratorImpl) *LetterProcessorImpl {
return &LetterProcessorImpl{letterRepo: letterRepo, attachRepo: attachRepo, txManager: txManager, activity: activity, dispositionRepo: dispRepo, dispositionDeptRepo: dispDeptRepo, dispositionActionSelRepo: dispSelRepo, dispositionNoteRepo: noteRepo, discussionRepo: discussionRepo, settingRepo: settingRepo, recipientRepo: recipientRepo, departmentRepo: departmentRepo, userDeptRepo: userDeptRepo, priorityRepo: priorityRepo, institutionRepo: institutionRepo, dispActionRepo: dispActionRepo, numberGenerator: numberGenerator}
func NewLetterProcessor(letterRepo *repository.LetterIncomingRepository, attachRepo *repository.LetterIncomingAttachmentRepository, txManager *repository.TxManager, activity *ActivityLogProcessorImpl, dispRepo *repository.LetterIncomingDispositionRepository, dispDeptRepo *repository.LetterIncomingDispositionDepartmentRepository, dispSelRepo *repository.LetterDispositionActionSelectionRepository, noteRepo *repository.DispositionNoteRepository, discussionRepo *repository.LetterDiscussionRepository,
settingRepo *repository.AppSettingRepository,
recipientRepo *repository.LetterIncomingRecipientRepository,
outgoingRecipientRepo *repository.LetterOutgoingRecipientRepository,
departmentRepo *repository.DepartmentRepository,
userDeptRepo *repository.UserDepartmentRepository,
priorityRepo *repository.PriorityRepository,
institutionRepo *repository.InstitutionRepository,
dispActionRepo *repository.DispositionActionRepository,
numberGenerator *LetterNumberGeneratorImpl,
dispoRoutes *repository.DispositionRouteRepository) *LetterProcessorImpl {
return &LetterProcessorImpl{letterRepo: letterRepo, attachRepo: attachRepo, txManager: txManager,
activity: activity, dispositionRepo: dispRepo, dispositionDeptRepo: dispDeptRepo,
dispositionActionSelRepo: dispSelRepo, dispositionNoteRepo: noteRepo,
discussionRepo: discussionRepo, settingRepo: settingRepo, recipientRepo: recipientRepo,
outgoingRecipientRepo: outgoingRecipientRepo,
departmentRepo: departmentRepo, userDeptRepo: userDeptRepo, priorityRepo: priorityRepo,
institutionRepo: institutionRepo, dispActionRepo: dispActionRepo, numberGenerator: numberGenerator,
dispoRoutes: dispoRoutes}
}
func (p *LetterProcessorImpl) CreateIncomingLetter(ctx context.Context, req *contract.CreateIncomingLetterRequest) (*contract.IncomingLetterResponse, error) {
var result *contract.IncomingLetterResponse
err := p.txManager.WithTransaction(ctx, func(txCtx context.Context) error {
userID := appcontext.FromGinContext(txCtx).UserID
userID := appcontext.FromGinContext(ctx).UserID
letterNumber, err := p.numberGenerator.GenerateNumber(
txCtx,
contract.SettingIncomingLetterPrefix,
contract.SettingIncomingLetterSequence,
"ESLI",
)
if err != nil {
return err
}
entity := &entities.LetterIncoming{
LetterNumber: req.LetterNumber,
ReferenceNumber: req.ReferenceNumber,
Subject: req.Subject,
Description: req.Description,
PriorityID: req.PriorityID,
SenderInstitutionID: req.SenderInstitutionID,
ReceivedDate: req.ReceivedDate,
DueDate: req.DueDate,
Status: entities.LetterIncomingStatusNew,
CreatedBy: userID,
}
entity := &entities.LetterIncoming{
ReferenceNumber: req.ReferenceNumber,
Subject: req.Subject,
Description: req.Description,
PriorityID: req.PriorityID,
SenderInstitutionID: req.SenderInstitutionID,
ReceivedDate: req.ReceivedDate,
DueDate: req.DueDate,
Status: entities.LetterIncomingStatusNew,
CreatedBy: userID,
}
entity.LetterNumber = letterNumber
if err := p.letterRepo.Create(txCtx, entity); err != nil {
return err
}
defaultDeptCodes := []string{}
if s, err := p.settingRepo.Get(txCtx, contract.SettingIncomingLetterRecipients); err == nil {
if arr, ok := s.Value["department_codes"].([]interface{}); ok {
for _, it := range arr {
if str, ok := it.(string); ok {
defaultDeptCodes = append(defaultDeptCodes, str)
}
}
}
}
depIDs := make([]uuid.UUID, 0, len(defaultDeptCodes))
for _, code := range defaultDeptCodes {
dep, err := p.departmentRepo.GetByCode(txCtx, code)
if err != nil {
continue
}
depIDs = append(depIDs, dep.ID)
}
userMemberships, _ := p.userDeptRepo.ListActiveByDepartmentIDs(txCtx, depIDs)
var recipients []entities.LetterIncomingRecipient
mapsUsers := map[string]bool{}
for _, row := range userMemberships {
uid := row.UserID
if _, ok := mapsUsers[uid.String()]; !ok {
recipients = append(recipients, entities.LetterIncomingRecipient{LetterID: entity.ID, RecipientUserID: &uid, RecipientDepartmentID: &row.DepartmentID, Status: entities.RecipientStatusNew})
}
mapsUsers[uid.String()] = true
}
if len(recipients) > 0 {
if err := p.recipientRepo.CreateBulk(txCtx, recipients); err != nil {
return err
}
}
if p.activity != nil {
action := "letter.created"
if err := p.activity.Log(txCtx, entity.ID, action, &userID, nil, nil, nil, nil, nil, map[string]interface{}{"letter_number": letterNumber}); err != nil {
return err
}
}
attachments := make([]entities.LetterIncomingAttachment, 0, len(req.Attachments))
for _, a := range req.Attachments {
attachments = append(attachments, entities.LetterIncomingAttachment{LetterID: entity.ID, FileURL: a.FileURL, FileName: a.FileName, FileType: a.FileType, UploadedBy: &userID})
}
if len(attachments) > 0 {
if err := p.attachRepo.CreateBulk(txCtx, attachments); err != nil {
return err
}
if p.activity != nil {
action := "attachment.uploaded"
for _, a := range attachments {
ctxMap := map[string]interface{}{"file_name": a.FileName, "file_type": a.FileType}
if err := p.activity.Log(txCtx, entity.ID, action, &userID, nil, nil, nil, nil, nil, ctxMap); err != nil {
return err
}
}
}
}
savedAttachments, _ := p.attachRepo.ListByLetter(txCtx, entity.ID)
var pr *entities.Priority
if entity.PriorityID != nil {
if p.priorityRepo != nil {
if got, err := p.priorityRepo.Get(txCtx, *entity.PriorityID); err == nil {
pr = got
}
}
}
var inst *entities.Institution
if entity.SenderInstitutionID != nil {
if p.institutionRepo != nil {
if got, err := p.institutionRepo.Get(txCtx, *entity.SenderInstitutionID); err == nil {
inst = got
}
}
}
result = transformer.LetterEntityToContract(entity, savedAttachments, pr, inst)
return nil
})
if err != nil {
if err := p.letterRepo.Create(ctx, entity); err != nil {
return nil, err
}
return result, nil
if err := p.createAttachments(ctx, entity.ID, req.Attachments, userID); err != nil {
return nil, err
}
return p.buildLetterResponse(ctx, entity)
}
func (p *LetterProcessorImpl) GetIncomingLetterByID(ctx context.Context, id uuid.UUID) (*contract.IncomingLetterResponse, error) {
// Get current user ID from context
userID := appcontext.FromGinContext(ctx).UserID
entity, err := p.letterRepo.Get(ctx, id)
if err != nil {
return nil, err
@@ -177,45 +104,128 @@ func (p *LetterProcessorImpl) GetIncomingLetterByID(ctx context.Context, id uuid
inst = got
}
}
return transformer.LetterEntityToContract(entity, atts, pr, inst), nil
// Check if letter is read by current user
isRead := false
if p.recipientRepo != nil {
if recipient, err := p.recipientRepo.GetByLetterAndUser(ctx, id, userID); err == nil {
isRead = recipient.ReadAt != nil
}
}
resp := transformer.LetterEntityToContract(entity, atts, pr, inst)
resp.IsRead = isRead
// Include created_by if the current user is the creator
if entity.CreatedBy == userID {
resp.CreatedBy = entity.CreatedBy
}
return resp, nil
}
func (p *LetterProcessorImpl) ListIncomingLetters(ctx context.Context, req *contract.ListIncomingLettersRequest) (*contract.ListIncomingLettersResponse, error) {
page, limit := req.Page, req.Limit
func (p *LetterProcessorImpl) GetLetterUnreadCounts(ctx context.Context) (*contract.LetterUnreadCountResponse, error) {
userID := appcontext.FromGinContext(ctx).UserID
filter := repository.ListIncomingLettersFilter{
Status: req.Status,
Query: req.Query,
DepartmentID: req.DepartmentID,
}
list, total, err := p.letterRepo.List(ctx, filter, limit, (page-1)*limit)
if err != nil {
return nil, err
}
respList := make([]contract.IncomingLetterResponse, 0, len(list))
for _, e := range list {
atts, _ := p.attachRepo.ListByLetter(ctx, e.ID)
var pr *entities.Priority
if e.PriorityID != nil && p.priorityRepo != nil {
if got, err := p.priorityRepo.Get(ctx, *e.PriorityID); err == nil {
pr = got
}
incomingUnread := 0
if p.recipientRepo != nil {
if count, err := p.recipientRepo.CountUnreadByUser(ctx, userID); err == nil {
incomingUnread = count
}
var inst *entities.Institution
if e.SenderInstitutionID != nil && p.institutionRepo != nil {
if got, err := p.institutionRepo.Get(ctx, *e.SenderInstitutionID); err == nil {
inst = got
}
}
resp := transformer.LetterEntityToContract(&e, atts, pr, inst)
respList = append(respList, *resp)
}
return &contract.ListIncomingLettersResponse{Letters: respList, Pagination: transformer.CreatePaginationResponse(int(total), page, limit)}, nil
outgoingUnread := 0
if p.outgoingRecipientRepo != nil {
if count, err := p.outgoingRecipientRepo.CountUnreadByUser(ctx, userID); err == nil {
outgoingUnread = count
}
}
response := &contract.LetterUnreadCountResponse{}
response.IncomingLetter.Unread = incomingUnread
response.OutgoingLetter.Unread = outgoingUnread
return response, nil
}
func (p *LetterProcessorImpl) MarkIncomingLetterAsRead(ctx context.Context, letterID uuid.UUID) (*contract.MarkLetterReadResponse, error) {
// Get current user ID from context
userID := appcontext.FromGinContext(ctx).UserID
// Mark the letter as read for the current user
if p.recipientRepo != nil {
if err := p.recipientRepo.MarkAsRead(ctx, letterID, userID); err != nil {
return &contract.MarkLetterReadResponse{
Success: false,
Message: "Failed to mark letter as read",
}, err
}
}
return &contract.MarkLetterReadResponse{
Success: true,
Message: "Letter marked as read successfully",
}, nil
}
func (p *LetterProcessorImpl) MarkOutgoingLetterAsRead(ctx context.Context, letterID uuid.UUID) (*contract.MarkLetterReadResponse, error) {
// Get current user ID from context
userID := appcontext.FromGinContext(ctx).UserID
// Mark the letter as read for the current user
if p.outgoingRecipientRepo != nil {
if err := p.outgoingRecipientRepo.MarkAsRead(ctx, letterID, userID); err != nil {
return &contract.MarkLetterReadResponse{
Success: false,
Message: "Failed to mark letter as read",
}, err
}
}
return &contract.MarkLetterReadResponse{
Success: true,
Message: "Letter marked as read successfully",
}, nil
}
func (p *LetterProcessorImpl) ListIncomingLetters(ctx context.Context, filter repository.ListIncomingLettersFilter, page, limit int) ([]entities.LetterIncoming, int64, error) {
// Just fetch the raw data
return p.letterRepo.List(ctx, filter, limit, (page-1)*limit)
}
func (p *LetterProcessorImpl) GetBatchAttachments(ctx context.Context, letterIDs []uuid.UUID) (map[uuid.UUID][]entities.LetterIncomingAttachment, error) {
if p.attachRepo == nil || len(letterIDs) == 0 {
return make(map[uuid.UUID][]entities.LetterIncomingAttachment), nil
}
return p.attachRepo.ListByLetterIDs(ctx, letterIDs)
}
func (p *LetterProcessorImpl) GetBatchPriorities(ctx context.Context, priorityIDs []uuid.UUID) (map[uuid.UUID]*entities.Priority, error) {
if p.priorityRepo == nil || len(priorityIDs) == 0 {
return make(map[uuid.UUID]*entities.Priority), nil
}
return p.priorityRepo.GetByIDs(ctx, priorityIDs)
}
func (p *LetterProcessorImpl) GetBatchInstitutions(ctx context.Context, institutionIDs []uuid.UUID) (map[uuid.UUID]*entities.Institution, error) {
if p.institutionRepo == nil || len(institutionIDs) == 0 {
return make(map[uuid.UUID]*entities.Institution), nil
}
return p.institutionRepo.GetByIDs(ctx, institutionIDs)
}
func (p *LetterProcessorImpl) GetBatchRecipientsByUser(ctx context.Context, letterIDs []uuid.UUID, userID uuid.UUID) (map[uuid.UUID]*entities.LetterIncomingRecipient, error) {
if p.recipientRepo == nil || len(letterIDs) == 0 {
return make(map[uuid.UUID]*entities.LetterIncomingRecipient), nil
}
return p.recipientRepo.GetByLetterIDsAndUser(ctx, letterIDs, userID)
}
func (p *LetterProcessorImpl) CountUnreadByUser(ctx context.Context, userID uuid.UUID) (int, error) {
if p.recipientRepo == nil {
return 0, nil
}
return p.recipientRepo.CountUnreadByUser(ctx, userID)
}
func (p *LetterProcessorImpl) UpdateIncomingLetter(ctx context.Context, id uuid.UUID, req *contract.UpdateIncomingLetterRequest) (*contract.IncomingLetterResponse, error) {
@@ -304,6 +314,18 @@ func (p *LetterProcessorImpl) CreateDispositions(ctx context.Context, req *contr
err := p.txManager.WithTransaction(ctx, func(txCtx context.Context) error {
userID := appcontext.FromGinContext(txCtx).UserID
existingDispDepts, err := p.dispositionDeptRepo.GetByLetterAndDepartment(txCtx, req.LetterID, req.FromDepartment)
if err == nil && len(existingDispDepts) > 0 {
for _, existingDispDept := range existingDispDepts {
if existingDispDept.Status == entities.DispositionDepartmentStatusPending {
existingDispDept.Status = entities.DispositionDepartmentStatusDispositioned
if err := p.dispositionDeptRepo.Update(txCtx, &existingDispDept); err != nil {
return err
}
}
}
}
disp := entities.LetterIncomingDisposition{
LetterID: req.LetterID,
DepartmentID: &req.FromDepartment,
@@ -318,7 +340,9 @@ func (p *LetterProcessorImpl) CreateDispositions(ctx context.Context, req *contr
for _, toDept := range req.ToDepartmentIDs {
dispDepartments = append(dispDepartments, entities.LetterIncomingDispositionDepartment{
LetterIncomingDispositionID: disp.ID,
LetterIncomingID: req.LetterID,
DepartmentID: toDept,
Status: entities.DispositionDepartmentStatusPending,
})
}
@@ -487,3 +511,57 @@ func (p *LetterProcessorImpl) UpdateDiscussion(ctx context.Context, letterID uui
}
return out, nil
}
func (p *LetterProcessorImpl) createAttachments(ctx context.Context, letterID uuid.UUID, attachments []contract.CreateIncomingLetterAttachment, userID uuid.UUID) error {
if len(attachments) == 0 {
return nil
}
attachmentEntities := make([]entities.LetterIncomingAttachment, 0, len(attachments))
for _, a := range attachments {
attachmentEntities = append(attachmentEntities, entities.LetterIncomingAttachment{
LetterID: letterID,
FileURL: a.FileURL,
FileName: a.FileName,
FileType: a.FileType,
UploadedBy: &userID,
})
}
if err := p.attachRepo.CreateBulk(ctx, attachmentEntities); err != nil {
return err
}
// Attachment logging will be handled by service layer
return nil
}
func (p *LetterProcessorImpl) buildLetterResponse(ctx context.Context, entity *entities.LetterIncoming) (*contract.IncomingLetterResponse, error) {
savedAttachments, _ := p.attachRepo.ListByLetter(ctx, entity.ID)
var pr *entities.Priority
if entity.PriorityID != nil && p.priorityRepo != nil {
if got, err := p.priorityRepo.Get(ctx, *entity.PriorityID); err == nil {
pr = got
}
}
var inst *entities.Institution
if entity.SenderInstitutionID != nil && p.institutionRepo != nil {
if got, err := p.institutionRepo.Get(ctx, *entity.SenderInstitutionID); err == nil {
inst = got
}
}
return transformer.LetterEntityToContract(entity, savedAttachments, pr, inst), nil
}
func (p *LetterProcessorImpl) BulkArchiveIncomingLetters(ctx context.Context, letterIDs []uuid.UUID) (int64, error) {
return p.letterRepo.BulkArchive(ctx, letterIDs)
}
// BulkArchiveIncomingLettersForUser archives letters for a specific user only
func (p *LetterProcessorImpl) BulkArchiveIncomingLettersForUser(ctx context.Context, letterIDs []uuid.UUID, userID uuid.UUID) (int64, error) {
return p.letterRepo.BulkArchiveForUser(ctx, letterIDs, userID)
}
@@ -0,0 +1,251 @@
package processor
import (
"context"
"fmt"
"time"
"eslogad-be/internal/appcontext"
"eslogad-be/internal/contract"
"eslogad-be/internal/entities"
"eslogad-be/internal/transformer"
"github.com/google/uuid"
)
func (p *LetterProcessorImpl) GetDepartmentDispositionStatus(ctx context.Context, req *contract.GetDepartmentDispositionStatusRequest) (*contract.ListDepartmentDispositionStatusResponse, error) {
dispositions, err := p.dispositionDeptRepo.GetByLetterIncomingID(ctx, req.LetterIncomingID)
if err != nil {
return nil, err
}
var response []contract.DepartmentDispositionStatusResponse
for _, disp := range dispositions {
letterResp := transformer.LetterIncomingEntityToContract(disp.LetterIncoming)
var fromDept *contract.DepartmentResponse
if disp.LetterIncomingDisposition != nil && disp.LetterIncomingDisposition.DepartmentID != nil {
fromDept = transformer.DepartmentEntityToContract(&disp.LetterIncomingDisposition.Department)
}
response = append(response, contract.DepartmentDispositionStatusResponse{
ID: disp.ID,
LetterID: disp.LetterIncomingID,
Letter: letterResp,
FromDepartmentID: disp.LetterIncomingDisposition.DepartmentID,
FromDepartment: fromDept,
ToDepartmentID: disp.DepartmentID,
ToDepartment: transformer.DepartmentEntityToContract(disp.Department),
Status: string(disp.Status),
Notes: disp.LetterIncomingDisposition.Notes,
ReadAt: disp.ReadAt,
CompletedAt: disp.CompletedAt,
CreatedAt: disp.CreatedAt,
UpdatedAt: disp.UpdatedAt,
})
}
return &contract.ListDepartmentDispositionStatusResponse{
Dispositions: response,
Pagination: contract.PaginationResponse{
TotalCount: len(response),
Page: 1,
Limit: len(response),
TotalPages: 1,
},
}, nil
}
func (p *LetterProcessorImpl) UpdateDispositionStatus(ctx context.Context, req *contract.UpdateDispositionStatusRequest) (*contract.DepartmentDispositionStatusResponse, error) {
var result *contract.DepartmentDispositionStatusResponse
err := p.txManager.WithTransaction(ctx, func(txCtx context.Context) error {
userID := appcontext.FromGinContext(txCtx).UserID
departmentID := appcontext.FromGinContext(txCtx).DepartmentID
dispDept, err := p.dispositionDeptRepo.GetByDispositionAndDepartment(txCtx, req.LetterIncomingID, departmentID)
if err != nil {
return err
}
notes := ""
if req.Notes != nil {
notes = *req.Notes
}
if err := p.updateDispositionDepartmentStatus(txCtx, dispDept.ID, req.Status, notes); err != nil {
return err
}
p.activity.LogLetterDispositionStatusUpdate(txCtx, req.LetterIncomingID, userID, req.Status)
if err := p.checkAndUpdateLetterCompletionStatus(txCtx, req.LetterIncomingID); err != nil {
return err
}
updatedDispDept, err := p.dispositionDeptRepo.GetByID(txCtx, dispDept.ID)
if err != nil {
return err
}
result = p.buildDispositionStatusResponse(updatedDispDept)
return nil
})
if err != nil {
return nil, err
}
return result, nil
}
// updateDispositionDepartmentStatus updates the status of a disposition department
func (p *LetterProcessorImpl) updateDispositionDepartmentStatus(ctx context.Context, dispDeptID uuid.UUID, status, notes string) error {
now := time.Now()
var dispositionStatus entities.LetterIncomingDispositionDepartmentStatus
var readAt, completedAt *time.Time
switch status {
case "completed":
dispositionStatus = entities.DispositionDepartmentStatusCompleted
completedAt = &now
readAt = &now // Mark as read when completing
case "read":
dispositionStatus = entities.DispositionDepartmentStatusRead
readAt = &now
default:
dispositionStatus = entities.DispositionDepartmentStatusPending
}
return p.dispositionDeptRepo.UpdateStatus(ctx, dispDeptID, dispositionStatus, notes, readAt, completedAt)
}
// addDispositionNoteIfProvided adds a note to the disposition if provided
func (p *LetterProcessorImpl) addDispositionNoteIfProvided(ctx context.Context, dispositionID uuid.UUID, userID uuid.UUID, notes *string) error {
if notes == nil || *notes == "" {
return nil
}
note := &entities.DispositionNote{
DispositionID: dispositionID,
UserID: &userID,
Note: *notes,
}
return p.dispositionNoteRepo.Create(ctx, note)
}
func (p *LetterProcessorImpl) checkAndUpdateLetterCompletionStatus(ctx context.Context, letterIncomingID uuid.UUID) error {
dispositions, err := p.dispositionDeptRepo.GetByLetterIncomingID(ctx, letterIncomingID)
if err != nil {
return err
}
allCompleted := true
for _, disp := range dispositions {
if disp.Status == entities.DispositionDepartmentStatusPending {
allCompleted = false
break
}
}
if allCompleted && len(dispositions) > 0 {
letter, err := p.letterRepo.GetByID(ctx, letterIncomingID)
if err != nil {
return err
}
letter.Status = "completed"
if err := p.letterRepo.Update(ctx, letter); err != nil {
return err
}
}
return nil
}
// buildDispositionStatusResponse builds the response for disposition status
func (p *LetterProcessorImpl) buildDispositionStatusResponse(dispDept *entities.LetterIncomingDispositionDepartment) *contract.DepartmentDispositionStatusResponse {
letterResp := transformer.LetterIncomingEntityToContract(dispDept.LetterIncoming)
var fromDept *contract.DepartmentResponse
if dispDept.LetterIncomingDisposition != nil && dispDept.LetterIncomingDisposition.DepartmentID != nil {
fromDept = transformer.DepartmentEntityToContract(&dispDept.LetterIncomingDisposition.Department)
}
return &contract.DepartmentDispositionStatusResponse{
ID: dispDept.ID,
LetterID: dispDept.LetterIncomingID,
Letter: letterResp,
FromDepartmentID: dispDept.LetterIncomingDisposition.DepartmentID,
FromDepartment: fromDept,
ToDepartmentID: dispDept.DepartmentID,
ToDepartment: transformer.DepartmentEntityToContract(dispDept.Department),
Status: string(dispDept.Status),
Notes: dispDept.LetterIncomingDisposition.Notes,
ReadAt: dispDept.ReadAt,
CompletedAt: dispDept.CompletedAt,
CreatedAt: dispDept.CreatedAt,
UpdatedAt: dispDept.UpdatedAt,
}
}
func (p *LetterProcessorImpl) GetLetterCTA(ctx context.Context, letterIncomingID uuid.UUID, departmentID uuid.UUID) (*contract.LetterCTAResponse, error) {
letter, err := p.letterRepo.GetByID(ctx, letterIncomingID)
if err != nil {
return nil, err
}
response := &contract.LetterCTAResponse{
LetterIncomingID: letterIncomingID,
Actions: []contract.LetterCTAAction{},
Message: "",
}
isEligibleForDispo, err := p.dispoRoutes.IsEligibleForDisposition(ctx, departmentID)
if err != nil {
return nil, err
}
if letter.Status == "completed" || letter.Status == "archived" {
response.Message = "Letter is no longer accepting actions"
return response, nil
}
dispDepts, err := p.dispositionDeptRepo.GetByLetterAndDepartment(ctx, letterIncomingID, departmentID)
if err != nil {
return nil, err
}
if len(dispDepts) == 0 {
response.Message = "Your department is not a recipient of this letter"
return response, nil
}
for _, dispDept := range dispDepts {
if dispDept.Status == entities.DispositionDepartmentStatusPending {
response.DispositionID = &dispDept.LetterIncomingDispositionID
currentStatus := string(dispDept.Status)
response.CurrentStatus = &currentStatus
if isEligibleForDispo {
response.Actions = append(response.Actions, contract.LetterCTAAction{
Type: "create_disposition",
Label: "Disposisi",
Path: fmt.Sprintf("/api/v1/letters/%s/dispositions", letterIncomingID),
Method: "POST",
Description: "Create a new disposition for this letter",
})
}
response.Actions = append(response.Actions, contract.LetterCTAAction{
Type: "update_status",
Label: "Tindak Lanjut",
Path: fmt.Sprintf("/api/v1/letters/dispositions/%s/status", response.LetterIncomingID),
Method: "PUT",
Description: "Update the status of your disposition",
})
}
}
return response, nil
}
@@ -0,0 +1,361 @@
package processor
import (
"context"
"fmt"
"net/url"
"eslogad-be/internal/config"
"eslogad-be/internal/contract"
"eslogad-be/internal/entities"
"github.com/google/uuid"
novu "github.com/novuhq/go-novu/lib"
)
type NotificationProcessor interface {
// User management
CreateSubscriber(ctx context.Context, user *entities.User) error
UpdateSubscriber(ctx context.Context, user *entities.User) error
DeleteSubscriber(ctx context.Context, userID uuid.UUID) error
CreateSubscriberFromContract(ctx context.Context, user *contract.UserResponse) error
BulkCreateSubscribers(ctx context.Context, users []*entities.User) error
// Letter notifications
SendIncomingLetterNotification(ctx context.Context, letterID uuid.UUID, recipientUserID uuid.UUID, subject string, body string) error
}
type NotificationProcessorImpl struct {
provider NotificationProvider
workflowID string
}
func NewNotificationProcessor(provider NotificationProvider, workflowID string) *NotificationProcessorImpl {
return &NotificationProcessorImpl{
provider: provider,
workflowID: workflowID,
}
}
func (p *NotificationProcessorImpl) CreateSubscriber(ctx context.Context, user *entities.User) error {
return p.provider.CreateSubscriber(ctx, user)
}
func (p *NotificationProcessorImpl) UpdateSubscriber(ctx context.Context, user *entities.User) error {
return p.provider.UpdateSubscriber(ctx, user)
}
func (p *NotificationProcessorImpl) DeleteSubscriber(ctx context.Context, userID uuid.UUID) error {
return p.provider.DeleteSubscriber(ctx, userID)
}
func (p *NotificationProcessorImpl) CreateSubscriberFromContract(ctx context.Context, user *contract.UserResponse) error {
return p.provider.CreateSubscriberFromContract(ctx, user)
}
func (p *NotificationProcessorImpl) BulkCreateSubscribers(ctx context.Context, users []*entities.User) error {
return p.provider.BulkCreateSubscribers(ctx, users)
}
func (p *NotificationProcessorImpl) SendIncomingLetterNotification(ctx context.Context, letterID uuid.UUID, recipientUserID uuid.UUID, subject string, body string) error {
// Ensure subscriber exists
if err := p.provider.EnsureSubscriberExists(ctx, recipientUserID); err != nil {
return fmt.Errorf("failed to ensure subscriber exists: %w", err)
}
// Build notification URL
url := fmt.Sprintf("/en/apps/surat-menyurat/masuk-detail/%s", letterID.String())
// Use workflow ID from config (defaults to "notification-dashbpard")
workflowID := p.workflowID
if workflowID == "" {
workflowID = "notification-dashbpard"
}
// Send notification
return p.provider.SendNotification(ctx, NotificationPayload{
RecipientID: recipientUserID,
EventName: workflowID,
Data: map[string]interface{}{
"subject": subject,
"body": body,
"url": url,
},
})
}
// NotificationProvider interface for different notification services
type NotificationProvider interface {
// User management
CreateSubscriber(ctx context.Context, user *entities.User) error
UpdateSubscriber(ctx context.Context, user *entities.User) error
DeleteSubscriber(ctx context.Context, userID uuid.UUID) error
CreateSubscriberFromContract(ctx context.Context, user *contract.UserResponse) error
BulkCreateSubscribers(ctx context.Context, users []*entities.User) error
// Core notification methods
EnsureSubscriberExists(ctx context.Context, userID uuid.UUID) error
SendNotification(ctx context.Context, payload NotificationPayload) error
}
type NotificationPayload struct {
RecipientID uuid.UUID
EventName string
Data map[string]interface{}
}
// NovuProvider implements NotificationProvider using Novu
type NovuProvider struct {
client *novu.APIClient
config *config.NovuConfig
}
func NewNovuProvider(cfg *config.NovuConfig) *NovuProvider {
if cfg.APIKey == "" {
return &NovuProvider{
client: nil,
config: cfg,
}
}
// Create Novu config with backend URL
novuConfig := &novu.Config{}
if cfg.BaseURL != "" {
backendURL, err := url.Parse(cfg.BaseURL)
if err == nil {
novuConfig.BackendURL = backendURL
}
}
client := novu.NewAPIClient(cfg.APIKey, novuConfig)
return &NovuProvider{
client: client,
config: cfg,
}
}
func (p *NovuProvider) CreateSubscriber(ctx context.Context, user *entities.User) error {
if p.client == nil {
return fmt.Errorf("novu client not initialized")
}
subscriberID := user.ID.String()
data := map[string]interface{}{
"userId": user.ID.String(),
"email": user.Email,
"isActive": user.IsActive,
"createdAt": user.CreatedAt,
}
if user.Departments != nil && len(user.Departments) > 0 {
depts := make([]map[string]interface{}, len(user.Departments))
for i, dept := range user.Departments {
depts[i] = map[string]interface{}{
"id": dept.ID.String(),
"name": dept.Name,
"code": dept.Code,
}
}
data["departments"] = depts
}
subscriber := novu.SubscriberPayload{
Email: user.Email,
FirstName: user.Name,
LastName: "",
Phone: "",
Avatar: "",
Data: data,
}
_, err := p.client.SubscriberApi.Identify(ctx, subscriberID, subscriber)
if err != nil {
return fmt.Errorf("failed to create subscriber: %w", err)
}
return nil
}
func (p *NovuProvider) UpdateSubscriber(ctx context.Context, user *entities.User) error {
if p.client == nil {
return fmt.Errorf("novu client not initialized")
}
subscriberID := user.ID.String()
data := map[string]interface{}{
"userId": user.ID.String(),
"email": user.Email,
"isActive": user.IsActive,
"updatedAt": user.UpdatedAt,
}
if user.Departments != nil && len(user.Departments) > 0 {
depts := make([]map[string]interface{}, len(user.Departments))
for i, dept := range user.Departments {
depts[i] = map[string]interface{}{
"id": dept.ID.String(),
"name": dept.Name,
"code": dept.Code,
}
}
data["departments"] = depts
}
updateData := novu.SubscriberPayload{
Email: user.Email,
FirstName: user.Name,
LastName: "",
Phone: "",
Avatar: "",
Data: data,
}
_, err := p.client.SubscriberApi.Update(ctx, subscriberID, updateData)
if err != nil {
return fmt.Errorf("failed to update subscriber: %w", err)
}
return nil
}
func (p *NovuProvider) DeleteSubscriber(ctx context.Context, userID uuid.UUID) error {
if p.client == nil {
return fmt.Errorf("novu client not initialized")
}
subscriberID := userID.String()
_, err := p.client.SubscriberApi.Delete(ctx, subscriberID)
if err != nil {
return fmt.Errorf("failed to delete subscriber: %w", err)
}
return nil
}
func (p *NovuProvider) CreateSubscriberFromContract(ctx context.Context, user *contract.UserResponse) error {
if p.client == nil {
return fmt.Errorf("novu client not initialized")
}
subscriberID := user.ID.String()
data := map[string]interface{}{
"userId": user.ID.String(),
"email": user.Email,
"isActive": user.IsActive,
"createdAt": user.CreatedAt,
}
if user.Roles != nil && len(user.Roles) > 0 {
roles := make([]map[string]interface{}, len(user.Roles))
for i, role := range user.Roles {
roles[i] = map[string]interface{}{
"id": role.ID.String(),
"name": role.Name,
"code": role.Code,
}
}
data["roles"] = roles
}
if user.DepartmentResponse != nil && len(user.DepartmentResponse) > 0 {
depts := make([]map[string]interface{}, len(user.DepartmentResponse))
for i, dept := range user.DepartmentResponse {
depts[i] = map[string]interface{}{
"id": dept.ID.String(),
"name": dept.Name,
"code": dept.Code,
}
}
data["departments"] = depts
}
subscriber := novu.SubscriberPayload{
Email: user.Email,
FirstName: user.Name,
LastName: "",
Phone: "",
Avatar: "",
Data: data,
}
_, err := p.client.SubscriberApi.Identify(ctx, subscriberID, subscriber)
if err != nil {
return fmt.Errorf("failed to create subscriber from contract: %w", err)
}
return nil
}
func (p *NovuProvider) BulkCreateSubscribers(ctx context.Context, users []*entities.User) error {
if p.client == nil {
return fmt.Errorf("novu client not initialized")
}
var lastErr error
successCount := 0
for _, user := range users {
err := p.CreateSubscriber(ctx, user)
if err != nil {
lastErr = err
continue
}
successCount++
}
if lastErr != nil && successCount == 0 {
return fmt.Errorf("failed to create any subscribers, last error: %w", lastErr)
}
if lastErr != nil {
return fmt.Errorf("created %d out of %d subscribers, last error: %w", successCount, len(users), lastErr)
}
return nil
}
func (p *NovuProvider) EnsureSubscriberExists(ctx context.Context, userID uuid.UUID) error {
if p.client == nil {
return fmt.Errorf("novu client not initialized")
}
subscriberID := userID.String()
// Check if subscriber exists
_, err := p.client.SubscriberApi.Get(ctx, subscriberID)
if err != nil {
// Subscriber doesn't exist, create a basic one
subscriber := novu.SubscriberPayload{
Email: fmt.Sprintf("%s@placeholder.com", subscriberID),
}
_, err = p.client.SubscriberApi.Identify(ctx, subscriberID, subscriber)
if err != nil {
return fmt.Errorf("failed to ensure subscriber exists: %w", err)
}
}
return nil
}
func (p *NovuProvider) SendNotification(ctx context.Context, payload NotificationPayload) error {
if p.client == nil {
return fmt.Errorf("novu client not initialized")
}
triggerPayload := novu.ITriggerPayloadOptions{
To: payload.RecipientID.String(),
Payload: payload.Data,
}
_, err := p.client.EventApi.Trigger(ctx, payload.EventName, triggerPayload)
if err != nil {
return fmt.Errorf("failed to send notification: %w", err)
}
return nil
}
+280
View File
@@ -0,0 +1,280 @@
package processor
import (
"context"
"fmt"
"net/url"
"eslogad-be/internal/config"
"eslogad-be/internal/contract"
"eslogad-be/internal/entities"
novu "github.com/novuhq/go-novu/lib"
"github.com/google/uuid"
)
type NovuProcessor interface {
CreateSubscriber(ctx context.Context, user *entities.User) error
UpdateSubscriber(ctx context.Context, user *entities.User) error
DeleteSubscriber(ctx context.Context, userID uuid.UUID) error
CreateSubscriberFromContract(ctx context.Context, user *contract.UserResponse) error
BulkCreateSubscribers(ctx context.Context, users []*entities.User) error
SendLetterNotification(ctx context.Context, letterID uuid.UUID, recipientUserID uuid.UUID, subject string, body string) error
}
type NovuProcessorImpl struct {
client *novu.APIClient
config *config.NovuConfig
}
func NewNovuProcessor(cfg *config.NovuConfig) *NovuProcessorImpl {
if cfg.APIKey == "" {
return &NovuProcessorImpl{
client: nil,
config: cfg,
}
}
// Create Novu config with backend URL
novuConfig := &novu.Config{}
if cfg.BaseURL != "" {
backendURL, err := url.Parse(cfg.BaseURL)
if err == nil {
novuConfig.BackendURL = backendURL
}
}
client := novu.NewAPIClient(cfg.APIKey, novuConfig)
return &NovuProcessorImpl{
client: client,
config: cfg,
}
}
func (p *NovuProcessorImpl) CreateSubscriber(ctx context.Context, user *entities.User) error {
if p.client == nil {
return fmt.Errorf("novu client not initialized")
}
subscriberID := user.ID.String()
data := map[string]interface{}{
"userId": user.ID.String(),
"email": user.Email,
"isActive": user.IsActive,
"createdAt": user.CreatedAt,
}
if user.Departments != nil && len(user.Departments) > 0 {
depts := make([]map[string]interface{}, len(user.Departments))
for i, dept := range user.Departments {
depts[i] = map[string]interface{}{
"id": dept.ID.String(),
"name": dept.Name,
"code": dept.Code,
}
}
data["departments"] = depts
}
subscriber := novu.SubscriberPayload{
Email: user.Email,
FirstName: user.Name,
LastName: "",
Phone: "",
Avatar: "",
Data: data,
}
_, err := p.client.SubscriberApi.Identify(ctx, subscriberID, subscriber)
if err != nil {
return fmt.Errorf("failed to create subscriber: %w", err)
}
return nil
}
func (p *NovuProcessorImpl) UpdateSubscriber(ctx context.Context, user *entities.User) error {
if p.client == nil {
return fmt.Errorf("novu client not initialized")
}
subscriberID := user.ID.String()
data := map[string]interface{}{
"userId": user.ID.String(),
"email": user.Email,
"isActive": user.IsActive,
"updatedAt": user.UpdatedAt,
}
if user.Departments != nil && len(user.Departments) > 0 {
depts := make([]map[string]interface{}, len(user.Departments))
for i, dept := range user.Departments {
depts[i] = map[string]interface{}{
"id": dept.ID.String(),
"name": dept.Name,
"code": dept.Code,
}
}
data["departments"] = depts
}
updateData := novu.SubscriberPayload{
Email: user.Email,
FirstName: user.Name,
LastName: "",
Phone: "",
Avatar: "",
Data: data,
}
_, err := p.client.SubscriberApi.Update(ctx, subscriberID, updateData)
if err != nil {
return fmt.Errorf("failed to update subscriber: %w", err)
}
return nil
}
func (p *NovuProcessorImpl) DeleteSubscriber(ctx context.Context, userID uuid.UUID) error {
if p.client == nil {
return fmt.Errorf("novu client not initialized")
}
subscriberID := userID.String()
_, err := p.client.SubscriberApi.Delete(ctx, subscriberID)
if err != nil {
return fmt.Errorf("failed to delete subscriber: %w", err)
}
return nil
}
func (p *NovuProcessorImpl) CreateSubscriberFromContract(ctx context.Context, user *contract.UserResponse) error {
if p.client == nil {
return fmt.Errorf("novu client not initialized")
}
subscriberID := user.ID.String()
data := map[string]interface{}{
"userId": user.ID.String(),
"email": user.Email,
"isActive": user.IsActive,
"createdAt": user.CreatedAt,
}
if user.Roles != nil && len(user.Roles) > 0 {
roles := make([]map[string]interface{}, len(user.Roles))
for i, role := range user.Roles {
roles[i] = map[string]interface{}{
"id": role.ID.String(),
"name": role.Name,
"code": role.Code,
}
}
data["roles"] = roles
}
if user.DepartmentResponse != nil && len(user.DepartmentResponse) > 0 {
depts := make([]map[string]interface{}, len(user.DepartmentResponse))
for i, dept := range user.DepartmentResponse {
depts[i] = map[string]interface{}{
"id": dept.ID.String(),
"name": dept.Name,
"code": dept.Code,
}
}
data["departments"] = depts
}
subscriber := novu.SubscriberPayload{
Email: user.Email,
FirstName: user.Name,
LastName: "",
Phone: "",
Avatar: "",
Data: data,
}
_, err := p.client.SubscriberApi.Identify(ctx, subscriberID, subscriber)
if err != nil {
return fmt.Errorf("failed to create subscriber from contract: %w", err)
}
return nil
}
func (p *NovuProcessorImpl) BulkCreateSubscribers(ctx context.Context, users []*entities.User) error {
if p.client == nil {
return fmt.Errorf("novu client not initialized")
}
var lastErr error
successCount := 0
for _, user := range users {
err := p.CreateSubscriber(ctx, user)
if err != nil {
lastErr = err
continue
}
successCount++
}
if lastErr != nil && successCount == 0 {
return fmt.Errorf("failed to create any subscribers, last error: %w", lastErr)
}
if lastErr != nil {
return fmt.Errorf("created %d out of %d subscribers, last error: %w", successCount, len(users), lastErr)
}
return nil
}
func (p *NovuProcessorImpl) SendLetterNotification(ctx context.Context, letterID uuid.UUID, recipientUserID uuid.UUID, subject string, body string) error {
if p.client == nil {
return fmt.Errorf("novu client not initialized")
}
subscriberID := recipientUserID.String()
// Check if subscriber exists, create if not
_, err := p.client.SubscriberApi.Get(ctx, subscriberID)
if err != nil {
// Subscriber doesn't exist, create a basic one
subscriber := novu.SubscriberPayload{
Email: fmt.Sprintf("%s@placeholder.com", subscriberID),
}
_, err = p.client.SubscriberApi.Identify(ctx, subscriberID, subscriber)
if err != nil {
return fmt.Errorf("failed to ensure subscriber exists: %w", err)
}
}
// Prepare notification payload
url := fmt.Sprintf("en/apps/surat-menyurat/masuk-detail/%s", letterID.String())
payload := map[string]interface{}{
"subject": subject,
"body": body,
"url": url,
}
// Trigger the notification
triggerPayload := novu.ITriggerPayloadOptions{
To: subscriberID,
Payload: payload,
}
_, err = p.client.EventApi.Trigger(ctx, "notification-dashboard", triggerPayload)
if err != nil {
return fmt.Errorf("failed to send letter notification: %w", err)
}
return nil
}
+96
View File
@@ -0,0 +1,96 @@
package processor
import (
"context"
"eslogad-be/internal/entities"
"eslogad-be/internal/repository"
"github.com/google/uuid"
)
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 RecipientProcessorImpl struct {
recipientRepo *repository.LetterIncomingRecipientRepository
settingRepo *repository.AppSettingRepository
departmentRepo *repository.DepartmentRepository
userDeptRepo *repository.UserDepartmentRepository
}
func NewRecipientProcessor(
recipientRepo *repository.LetterIncomingRecipientRepository,
settingRepo *repository.AppSettingRepository,
departmentRepo *repository.DepartmentRepository,
userDeptRepo *repository.UserDepartmentRepository,
) *RecipientProcessorImpl {
return &RecipientProcessorImpl{
recipientRepo: recipientRepo,
settingRepo: settingRepo,
departmentRepo: departmentRepo,
userDeptRepo: userDeptRepo,
}
}
func (p *RecipientProcessorImpl) CreateDefaultRecipients(ctx context.Context, letterID uuid.UUID) ([]entities.LetterIncomingRecipient, error) {
departmentIDs, err := p.settingRepo.GetDepartmentRecipients(ctx)
if err != nil {
return []entities.LetterIncomingRecipient{}, nil
}
if len(departmentIDs) == 0 {
return []entities.LetterIncomingRecipient{}, nil
}
return p.CreateRecipients(ctx, letterID, departmentIDs)
}
func (p *RecipientProcessorImpl) CreateRecipients(ctx context.Context, letterID uuid.UUID, departmentIDs []uuid.UUID) ([]entities.LetterIncomingRecipient, error) {
if len(departmentIDs) == 0 {
return []entities.LetterIncomingRecipient{}, nil
}
userMemberships, err := p.userDeptRepo.ListActiveByDepartmentIDs(ctx, departmentIDs)
if err != nil {
return nil, err
}
recipients := p.buildUniqueRecipients(letterID, userMemberships)
if len(recipients) > 0 {
if err := p.recipientRepo.CreateBulk(ctx, recipients); err != nil {
return nil, err
}
}
return recipients, nil
}
func (p *RecipientProcessorImpl) buildUniqueRecipients(letterID uuid.UUID, userMemberships []repository.UserDepartmentRow) []entities.LetterIncomingRecipient {
var recipients []entities.LetterIncomingRecipient
userMap := make(map[string]bool)
for _, membership := range userMemberships {
userIDStr := membership.UserID.String()
if !userMap[userIDStr] {
recipients = append(recipients, entities.LetterIncomingRecipient{
LetterID: letterID,
RecipientUserID: &membership.UserID,
RecipientDepartmentID: &membership.DepartmentID,
Status: entities.RecipientStatusNew,
})
userMap[userIDStr] = true
}
}
return recipients
}
func (p *RecipientProcessorImpl) CreateSingleRecipient(ctx context.Context, recipient *entities.LetterIncomingRecipient) error {
return p.recipientRepo.Create(ctx, recipient)
}
+50 -2
View File
@@ -14,8 +14,9 @@ import (
)
type UserProcessorImpl struct {
userRepo UserRepository
profileRepo UserProfileRepository
userRepo UserRepository
profileRepo UserProfileRepository
novuProcessor NovuProcessor
}
type UserProfileRepository interface {
@@ -35,6 +36,10 @@ func NewUserProcessor(
}
}
func (p *UserProcessorImpl) SetNovuProcessor(novuProcessor NovuProcessor) {
p.novuProcessor = novuProcessor
}
func (p *UserProcessorImpl) CreateUser(ctx context.Context, req *contract.CreateUserRequest) (*contract.UserResponse, error) {
existingUser, err := p.userRepo.GetByEmail(ctx, req.Email)
if err == nil && existingUser != nil {
@@ -65,6 +70,15 @@ func (p *UserProcessorImpl) CreateUser(ctx context.Context, req *contract.Create
}
_ = p.profileRepo.Create(ctx, profile)
// Create Novu subscriber
if p.novuProcessor != nil {
if err := p.novuProcessor.CreateSubscriber(ctx, userEntity); err != nil {
// Log error but don't fail user creation
// You might want to add proper logging here
_ = err
}
}
return transformer.EntityToContract(userEntity), nil
}
@@ -88,6 +102,13 @@ func (p *UserProcessorImpl) UpdateUser(ctx context.Context, id uuid.UUID, req *c
return nil, fmt.Errorf("failed to update user: %w", err)
}
// Update Novu subscriber
if p.novuProcessor != nil {
if err := p.novuProcessor.UpdateSubscriber(ctx, updated); err != nil {
_ = err
}
}
return transformer.EntityToContract(updated), nil
}
@@ -102,6 +123,14 @@ func (p *UserProcessorImpl) DeleteUser(ctx context.Context, id uuid.UUID) error
return fmt.Errorf("failed to delete user: %w", err)
}
// Delete Novu subscriber
if p.novuProcessor != nil {
if err := p.novuProcessor.DeleteSubscriber(ctx, id); err != nil {
// Log error but don't fail user deletion
_ = err
}
}
return nil
}
@@ -121,6 +150,25 @@ func (p *UserProcessorImpl) GetUserByID(ctx context.Context, id uuid.UUID) (*con
return resp, nil
}
// GetUserByIDLight retrieves user without relationships - optimized for auth checks
func (p *UserProcessorImpl) GetUserByIDLight(ctx context.Context, id uuid.UUID) (*contract.UserResponse, error) {
user, err := p.userRepo.GetByIDLight(ctx, id)
if err != nil {
return nil, fmt.Errorf("user not found: %w", err)
}
resp := &contract.UserResponse{
ID: user.ID,
Email: user.Email,
Name: user.Name,
IsActive: user.IsActive,
CreatedAt: user.CreatedAt,
UpdatedAt: user.UpdatedAt,
}
return resp, nil
}
func (p *UserProcessorImpl) GetUserByEmail(ctx context.Context, email string) (*contract.UserResponse, error) {
user, err := p.userRepo.GetByEmail(ctx, email)
if err != nil {
+1
View File
@@ -10,6 +10,7 @@ import (
type UserRepository interface {
Create(ctx context.Context, user *entities.User) error
GetByID(ctx context.Context, id uuid.UUID) (*entities.User, error)
GetByIDLight(ctx context.Context, id uuid.UUID) (*entities.User, error)
GetByEmail(ctx context.Context, email string) (*entities.User, error)
GetByRole(ctx context.Context, role entities.UserRole) ([]*entities.User, error)
GetActiveUsers(ctx context.Context, organizationID uuid.UUID) ([]*entities.User, error)