Init All Docs
This commit is contained in:
@@ -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,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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 = ¤tStatus
|
||||
|
||||
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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user