Init All Docs
This commit is contained in:
@@ -90,7 +90,7 @@ func (s *AuthServiceImpl) ValidateToken(tokenString string) (*contract.UserRespo
|
||||
return nil, fmt.Errorf("user account is deactivated")
|
||||
}
|
||||
|
||||
// Departments are now preloaded, so they're already in the response
|
||||
// Note: Departments are not loaded in light version, add if needed
|
||||
return userResponse, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sort"
|
||||
|
||||
"eslogad-be/internal/contract"
|
||||
"eslogad-be/internal/entities"
|
||||
@@ -19,19 +20,95 @@ func NewDispositionRouteService(repo *repository.DispositionRouteRepository) *Di
|
||||
return &DispositionRouteServiceImpl{repo: repo}
|
||||
}
|
||||
|
||||
func (s *DispositionRouteServiceImpl) Create(ctx context.Context, req *contract.CreateDispositionRouteRequest) (*contract.DispositionRouteResponse, error) {
|
||||
entity := &entities.DispositionRoute{FromDepartmentID: req.FromDepartmentID, ToDepartmentID: req.ToDepartmentID}
|
||||
// CreateOrUpdate handles bulk create or update of disposition routes
|
||||
func (s *DispositionRouteServiceImpl) CreateOrUpdate(ctx context.Context, req *contract.CreateDispositionRouteRequest) (*contract.BulkCreateDispositionRouteResponse, error) {
|
||||
// Set default values
|
||||
isActive := true
|
||||
if req.IsActive != nil {
|
||||
entity.IsActive = *req.IsActive
|
||||
isActive = *req.IsActive
|
||||
}
|
||||
|
||||
var allowedActions entities.JSONB
|
||||
if req.AllowedActions != nil {
|
||||
entity.AllowedActions = entities.JSONB(*req.AllowedActions)
|
||||
allowedActions = entities.JSONB(*req.AllowedActions)
|
||||
}
|
||||
if err := s.repo.Create(ctx, entity); err != nil {
|
||||
|
||||
// Perform bulk upsert
|
||||
created, updated, err := s.repo.BulkUpsert(ctx, req.FromDepartmentID, req.ToDepartmentIDs, isActive, allowedActions)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
resp := transformer.DispositionRoutesToContract([]entities.DispositionRoute{*entity})[0]
|
||||
return &resp, nil
|
||||
|
||||
// Fetch all routes for the from_department_id to return
|
||||
routes, err := s.repo.ListByFromDept(ctx, req.FromDepartmentID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Transform to response
|
||||
routeResponses := transformer.DispositionRoutesToContract(routes)
|
||||
|
||||
return &contract.BulkCreateDispositionRouteResponse{
|
||||
Created: created,
|
||||
Updated: updated,
|
||||
Routes: routeResponses,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Create maintains backward compatibility for single route creation
|
||||
func (s *DispositionRouteServiceImpl) Create(ctx context.Context, req *contract.CreateDispositionRouteRequest) (*contract.DispositionRouteResponse, error) {
|
||||
// If only one to_department_id is provided, create a single route
|
||||
if len(req.ToDepartmentIDs) == 1 {
|
||||
entity := &entities.DispositionRoute{
|
||||
FromDepartmentID: req.FromDepartmentID,
|
||||
ToDepartmentID: req.ToDepartmentIDs[0],
|
||||
}
|
||||
if req.IsActive != nil {
|
||||
entity.IsActive = *req.IsActive
|
||||
} else {
|
||||
entity.IsActive = true
|
||||
}
|
||||
if req.AllowedActions != nil {
|
||||
entity.AllowedActions = entities.JSONB(*req.AllowedActions)
|
||||
}
|
||||
|
||||
// Use upsert to handle create or update
|
||||
if err := s.repo.Upsert(ctx, entity); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Fetch the created/updated route
|
||||
route, err := s.repo.Get(ctx, entity.ID)
|
||||
if err != nil {
|
||||
// If we can't get by ID (new creation), try to get by from/to combination
|
||||
routes, err := s.repo.ListByFromDept(ctx, req.FromDepartmentID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, r := range routes {
|
||||
if r.ToDepartmentID == req.ToDepartmentIDs[0] {
|
||||
resp := transformer.DispositionRoutesToContract([]entities.DispositionRoute{r})[0]
|
||||
return &resp, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
resp := transformer.DispositionRoutesToContract([]entities.DispositionRoute{*route})[0]
|
||||
return &resp, nil
|
||||
}
|
||||
|
||||
// For multiple to_department_ids, use bulk create/update
|
||||
bulkResp, err := s.CreateOrUpdate(ctx, req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Return the first route as response for backward compatibility
|
||||
if len(bulkResp.Routes) > 0 {
|
||||
return &bulkResp.Routes[0], nil
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
func (s *DispositionRouteServiceImpl) Update(ctx context.Context, id uuid.UUID, req *contract.UpdateDispositionRouteRequest) (*contract.DispositionRouteResponse, error) {
|
||||
entity, err := s.repo.Get(ctx, id)
|
||||
@@ -68,3 +145,79 @@ func (s *DispositionRouteServiceImpl) ListByFromDept(ctx context.Context, from u
|
||||
func (s *DispositionRouteServiceImpl) SetActive(ctx context.Context, id uuid.UUID, active bool) error {
|
||||
return s.repo.SetActive(ctx, id, active)
|
||||
}
|
||||
|
||||
// ListGrouped returns all disposition routes grouped by from_department_id with clean department structure
|
||||
func (s *DispositionRouteServiceImpl) ListGrouped(ctx context.Context) (*contract.ListDispositionRoutesGroupedResponse, error) {
|
||||
// Get routes with department details
|
||||
routes, err := s.repo.ListAllGroupedWithDepartments(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Group routes by from_department_id and collect department info
|
||||
type groupedData struct {
|
||||
fromDept contract.DepartmentMapping
|
||||
toDepts []contract.DepartmentMapping
|
||||
toDeptMap map[uuid.UUID]bool // To avoid duplicates
|
||||
}
|
||||
|
||||
grouped := make(map[uuid.UUID]*groupedData)
|
||||
|
||||
for _, route := range routes {
|
||||
if _, exists := grouped[route.FromDepartmentID]; !exists {
|
||||
grouped[route.FromDepartmentID] = &groupedData{
|
||||
fromDept: contract.DepartmentMapping{
|
||||
ID: route.FromDepartmentID,
|
||||
Name: route.FromDepartment.Name,
|
||||
},
|
||||
toDepts: []contract.DepartmentMapping{},
|
||||
toDeptMap: make(map[uuid.UUID]bool),
|
||||
}
|
||||
}
|
||||
|
||||
// Add to_department if not already added (avoid duplicates)
|
||||
if !grouped[route.FromDepartmentID].toDeptMap[route.ToDepartmentID] {
|
||||
grouped[route.FromDepartmentID].toDepts = append(
|
||||
grouped[route.FromDepartmentID].toDepts,
|
||||
contract.DepartmentMapping{
|
||||
ID: route.ToDepartmentID,
|
||||
Name: route.ToDepartment.Name,
|
||||
},
|
||||
)
|
||||
grouped[route.FromDepartmentID].toDeptMap[route.ToDepartmentID] = true
|
||||
}
|
||||
}
|
||||
|
||||
// Convert to response format
|
||||
var dispositions []contract.DispositionRouteGroupedItem
|
||||
for _, data := range grouped {
|
||||
dispositions = append(dispositions, contract.DispositionRouteGroupedItem{
|
||||
FromDepartment: data.fromDept,
|
||||
ToDepartments: data.toDepts,
|
||||
})
|
||||
}
|
||||
|
||||
// Sort by from department name for consistent ordering
|
||||
sort.Slice(dispositions, func(i, j int) bool {
|
||||
return dispositions[i].FromDepartment.Name < dispositions[j].FromDepartment.Name
|
||||
})
|
||||
|
||||
return &contract.ListDispositionRoutesGroupedResponse{
|
||||
Dispositions: dispositions,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// ListAll returns all disposition routes with department details
|
||||
func (s *DispositionRouteServiceImpl) ListAll(ctx context.Context) (*contract.ListDispositionRoutesDetailedResponse, error) {
|
||||
routes, err := s.repo.ListAll(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
routeResponses := transformer.DispositionRoutesToContract(routes)
|
||||
|
||||
return &contract.ListDispositionRoutesDetailedResponse{
|
||||
Routes: routeResponses,
|
||||
Total: len(routeResponses),
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -32,8 +32,8 @@ func (s *FileServiceImpl) UploadProfileAvatar(ctx context.Context, userID uuid.U
|
||||
return "", err
|
||||
}
|
||||
ext := strings.ToLower(strings.TrimPrefix(filepath.Ext(filename), "."))
|
||||
if ext := mimeExtFromContentType(contentType); ext != "" {
|
||||
ext = ext
|
||||
if mimeExt := mimeExtFromContentType(contentType); mimeExt != "" {
|
||||
ext = mimeExt
|
||||
}
|
||||
key := buildObjectKey("profile", userID, ext)
|
||||
url, err := s.storage.Upload(ctx, s.profileBucket, key, content, contentType)
|
||||
@@ -50,8 +50,8 @@ func (s *FileServiceImpl) UploadDocument(ctx context.Context, userID uuid.UUID,
|
||||
return "", "", err
|
||||
}
|
||||
ext := strings.ToLower(strings.TrimPrefix(filepath.Ext(filename), "."))
|
||||
if ext := mimeExtFromContentType(contentType); ext != "" {
|
||||
ext = ext
|
||||
if mimeExt := mimeExtFromContentType(contentType); mimeExt != "" {
|
||||
ext = mimeExt
|
||||
}
|
||||
key := buildObjectKey("documents", userID, ext)
|
||||
url, err := s.storage.Upload(ctx, s.docBucket, key, content, contentType)
|
||||
|
||||
@@ -114,6 +114,11 @@ func (s *LetterOutgoingServiceImpl) GetOutgoingLetterByID(ctx context.Context, i
|
||||
}
|
||||
|
||||
func (s *LetterOutgoingServiceImpl) ListOutgoingLetters(ctx context.Context, req *contract.ListOutgoingLettersRequest) (*contract.ListOutgoingLettersResponse, error) {
|
||||
// Extract user context from gin context
|
||||
appCtx := appcontext.FromGinContext(ctx)
|
||||
userID := appCtx.UserID
|
||||
departmentID := appCtx.DepartmentID
|
||||
|
||||
offset := (req.Page - 1) * req.Limit
|
||||
if offset < 0 {
|
||||
offset = 0
|
||||
@@ -124,6 +129,11 @@ func (s *LetterOutgoingServiceImpl) ListOutgoingLetters(ctx context.Context, req
|
||||
DepartmentID: req.DepartmentID,
|
||||
ReceiverInstitutionID: req.ReceiverInstitutionID,
|
||||
PriorityID: req.PriorityID,
|
||||
UserID: &userID,
|
||||
}
|
||||
|
||||
if departmentID != uuid.Nil {
|
||||
filter.DepartmentID = &departmentID
|
||||
}
|
||||
|
||||
if req.Status != "" {
|
||||
@@ -152,16 +162,104 @@ func (s *LetterOutgoingServiceImpl) ListOutgoingLetters(ctx context.Context, req
|
||||
}
|
||||
}
|
||||
|
||||
// Apply access control overrides based on user context
|
||||
ApplyLetterFilterOverrides(ctx, &filter)
|
||||
filter.IsArchived = req.IsArchived
|
||||
|
||||
// Get raw letters data
|
||||
letters, total, err := s.processor.ListOutgoingLetters(ctx, filter, req.Limit, offset)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Collect IDs for batch loading
|
||||
letterIDs := make([]uuid.UUID, len(letters))
|
||||
priorityIDs := make(map[uuid.UUID]bool)
|
||||
institutionIDs := make(map[uuid.UUID]bool)
|
||||
|
||||
for i, letter := range letters {
|
||||
letterIDs[i] = letter.ID
|
||||
if letter.PriorityID != nil {
|
||||
priorityIDs[*letter.PriorityID] = true
|
||||
}
|
||||
if letter.ReceiverInstitutionID != nil {
|
||||
institutionIDs[*letter.ReceiverInstitutionID] = true
|
||||
}
|
||||
}
|
||||
|
||||
// Convert maps to slices
|
||||
priorityIDSlice := make([]uuid.UUID, 0, len(priorityIDs))
|
||||
for id := range priorityIDs {
|
||||
priorityIDSlice = append(priorityIDSlice, id)
|
||||
}
|
||||
|
||||
institutionIDSlice := make([]uuid.UUID, 0, len(institutionIDs))
|
||||
for id := range institutionIDs {
|
||||
institutionIDSlice = append(institutionIDSlice, id)
|
||||
}
|
||||
|
||||
// Parallel batch loading
|
||||
type batchResult struct {
|
||||
attachments map[uuid.UUID][]entities.LetterOutgoingAttachment
|
||||
recipients map[uuid.UUID][]entities.LetterOutgoingRecipient
|
||||
priorities map[uuid.UUID]*entities.Priority
|
||||
institutions map[uuid.UUID]*entities.Institution
|
||||
err error
|
||||
}
|
||||
|
||||
result := batchResult{}
|
||||
errChan := make(chan error, 4)
|
||||
|
||||
// Load attachments
|
||||
go func() {
|
||||
result.attachments, err = s.processor.GetBatchAttachments(ctx, letterIDs)
|
||||
errChan <- err
|
||||
}()
|
||||
|
||||
// Load recipients
|
||||
go func() {
|
||||
result.recipients, err = s.processor.GetBatchRecipients(ctx, letterIDs)
|
||||
errChan <- err
|
||||
}()
|
||||
|
||||
// Load priorities
|
||||
go func() {
|
||||
result.priorities, err = s.processor.GetBatchPriorities(ctx, priorityIDSlice)
|
||||
errChan <- err
|
||||
}()
|
||||
|
||||
// Load institutions
|
||||
go func() {
|
||||
result.institutions, err = s.processor.GetBatchInstitutions(ctx, institutionIDSlice)
|
||||
errChan <- err
|
||||
}()
|
||||
|
||||
// Wait for all goroutines and check for errors
|
||||
for i := 0; i < 4; i++ {
|
||||
if err := <-errChan; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
// Transform letters with batch loaded data
|
||||
items := make([]*contract.OutgoingLetterResponse, len(letters))
|
||||
for i, letter := range letters {
|
||||
// Attach batch loaded data to letter
|
||||
if attachments, ok := result.attachments[letter.ID]; ok {
|
||||
letter.Attachments = attachments
|
||||
}
|
||||
if recipients, ok := result.recipients[letter.ID]; ok {
|
||||
letter.Recipients = recipients
|
||||
}
|
||||
if letter.PriorityID != nil {
|
||||
if priority, ok := result.priorities[*letter.PriorityID]; ok {
|
||||
letter.Priority = priority
|
||||
}
|
||||
}
|
||||
if letter.ReceiverInstitutionID != nil {
|
||||
if institution, ok := result.institutions[*letter.ReceiverInstitutionID]; ok {
|
||||
letter.ReceiverInstitution = institution
|
||||
}
|
||||
}
|
||||
|
||||
items[i] = transformLetterToResponse(&letter)
|
||||
}
|
||||
|
||||
@@ -1361,3 +1459,16 @@ func ApplyLetterFilterOverrides(ctx context.Context, filter *repository.ListOutg
|
||||
filter.UserID = &appCtx.UserID
|
||||
}
|
||||
}
|
||||
|
||||
func (s *LetterOutgoingServiceImpl) BulkArchiveOutgoingLetters(ctx context.Context, letterIDs []uuid.UUID) (*contract.BulkArchiveLettersResponse, error) {
|
||||
archivedCount, err := s.processor.BulkArchiveOutgoingLetters(ctx, letterIDs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &contract.BulkArchiveLettersResponse{
|
||||
Success: true,
|
||||
Message: "Letters archived successfully",
|
||||
ArchivedCount: int(archivedCount),
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -2,42 +2,418 @@ package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"eslogad-be/internal/logger"
|
||||
"time"
|
||||
|
||||
"eslogad-be/internal/appcontext"
|
||||
"eslogad-be/internal/constant"
|
||||
"eslogad-be/internal/contract"
|
||||
"eslogad-be/internal/entities"
|
||||
"eslogad-be/internal/processor"
|
||||
"eslogad-be/internal/repository"
|
||||
"eslogad-be/internal/transformer"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
const (
|
||||
DefaultIncomingLetterID = "ESLI"
|
||||
)
|
||||
|
||||
type LetterProcessor interface {
|
||||
CreateIncomingLetter(ctx context.Context, req *contract.CreateIncomingLetterRequest) (*contract.IncomingLetterResponse, error)
|
||||
GetIncomingLetterByID(ctx context.Context, id uuid.UUID) (*contract.IncomingLetterResponse, error)
|
||||
ListIncomingLetters(ctx context.Context, req *contract.ListIncomingLettersRequest) (*contract.ListIncomingLettersResponse, error)
|
||||
ListIncomingLetters(ctx context.Context, filter repository.ListIncomingLettersFilter, page, limit int) ([]entities.LetterIncoming, int64, error)
|
||||
GetLetterUnreadCounts(ctx context.Context) (*contract.LetterUnreadCountResponse, error)
|
||||
MarkIncomingLetterAsRead(ctx context.Context, letterID uuid.UUID) (*contract.MarkLetterReadResponse, error)
|
||||
MarkOutgoingLetterAsRead(ctx context.Context, letterID uuid.UUID) (*contract.MarkLetterReadResponse, error)
|
||||
UpdateIncomingLetter(ctx context.Context, id uuid.UUID, req *contract.UpdateIncomingLetterRequest) (*contract.IncomingLetterResponse, error)
|
||||
SoftDeleteIncomingLetter(ctx context.Context, id uuid.UUID) error
|
||||
BulkArchiveIncomingLetters(ctx context.Context, letterIDs []uuid.UUID) (int64, error)
|
||||
BulkArchiveIncomingLettersForUser(ctx context.Context, letterIDs []uuid.UUID, userID uuid.UUID) (int64, error)
|
||||
|
||||
// Batch loading methods
|
||||
GetBatchAttachments(ctx context.Context, letterIDs []uuid.UUID) (map[uuid.UUID][]entities.LetterIncomingAttachment, error)
|
||||
GetBatchPriorities(ctx context.Context, priorityIDs []uuid.UUID) (map[uuid.UUID]*entities.Priority, error)
|
||||
GetBatchInstitutions(ctx context.Context, institutionIDs []uuid.UUID) (map[uuid.UUID]*entities.Institution, error)
|
||||
GetBatchRecipientsByUser(ctx context.Context, letterIDs []uuid.UUID, userID uuid.UUID) (map[uuid.UUID]*entities.LetterIncomingRecipient, error)
|
||||
CountUnreadByUser(ctx context.Context, userID uuid.UUID) (int, error)
|
||||
|
||||
CreateDispositions(ctx context.Context, req *contract.CreateLetterDispositionRequest) (*contract.ListDispositionsResponse, error)
|
||||
GetEnhancedDispositionsByLetter(ctx context.Context, letterID uuid.UUID) (*contract.ListEnhancedDispositionsResponse, error)
|
||||
|
||||
CreateDiscussion(ctx context.Context, letterID uuid.UUID, req *contract.CreateLetterDiscussionRequest) (*contract.LetterDiscussionResponse, error)
|
||||
UpdateDiscussion(ctx context.Context, letterID uuid.UUID, discussionID uuid.UUID, req *contract.UpdateLetterDiscussionRequest) (*contract.LetterDiscussionResponse, error)
|
||||
|
||||
GetDepartmentDispositionStatus(ctx context.Context, req *contract.GetDepartmentDispositionStatusRequest) (*contract.ListDepartmentDispositionStatusResponse, error)
|
||||
UpdateDispositionStatus(ctx context.Context, req *contract.UpdateDispositionStatusRequest) (*contract.DepartmentDispositionStatusResponse, error)
|
||||
|
||||
GetLetterCTA(ctx context.Context, letterID uuid.UUID, departmentID uuid.UUID) (*contract.LetterCTAResponse, error)
|
||||
}
|
||||
|
||||
type LetterServiceImpl struct {
|
||||
processor LetterProcessor
|
||||
processor LetterProcessor
|
||||
txManager *repository.TxManager
|
||||
numberGenerator NumberGenerator
|
||||
recipientProcessor RecipientProcessor
|
||||
activityLogger ActivityLogger
|
||||
letterDispositionProcessor LetterDispositionProcessor
|
||||
notificationProcessor processor.NotificationProcessor
|
||||
}
|
||||
|
||||
func NewLetterService(processor LetterProcessor) *LetterServiceImpl {
|
||||
return &LetterServiceImpl{processor: processor}
|
||||
type NumberGenerator interface {
|
||||
GenerateNumber(ctx context.Context, prefixKey, sequenceKey, defaultPrefix string) (string, error)
|
||||
}
|
||||
|
||||
type RecipientProcessor interface {
|
||||
CreateDefaultRecipients(ctx context.Context, letterID uuid.UUID) ([]entities.LetterIncomingRecipient, error)
|
||||
CreateSingleRecipient(ctx context.Context, recipient *entities.LetterIncomingRecipient) error
|
||||
}
|
||||
|
||||
type ActivityLogger interface {
|
||||
LogLetterCreated(ctx context.Context, letterID uuid.UUID, userID uuid.UUID, letterNumber string) error
|
||||
LogAttachmentUploaded(ctx context.Context, letterID uuid.UUID, userID uuid.UUID, fileName string, fileType string) error
|
||||
LogLetterDispositionStatusUpdate(ctx context.Context, letterID uuid.UUID, userID uuid.UUID, status string) error
|
||||
}
|
||||
|
||||
type LetterDispositionProcessor interface {
|
||||
CreateDispositions(ctx context.Context, req *contract.CreateLetterDispositionRequest) (*contract.ListDispositionsResponse, error)
|
||||
}
|
||||
|
||||
func NewLetterService(
|
||||
processor LetterProcessor,
|
||||
txManager *repository.TxManager,
|
||||
numberGenerator NumberGenerator,
|
||||
recipientProcessor RecipientProcessor,
|
||||
activityLogger ActivityLogger,
|
||||
letterDispositionProcessor LetterDispositionProcessor,
|
||||
notificationProcessor processor.NotificationProcessor,
|
||||
) *LetterServiceImpl {
|
||||
return &LetterServiceImpl{
|
||||
processor: processor,
|
||||
txManager: txManager,
|
||||
numberGenerator: numberGenerator,
|
||||
recipientProcessor: recipientProcessor,
|
||||
activityLogger: activityLogger,
|
||||
letterDispositionProcessor: letterDispositionProcessor,
|
||||
notificationProcessor: notificationProcessor,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *LetterServiceImpl) CreateIncomingLetter(ctx context.Context, req *contract.CreateIncomingLetterRequest) (*contract.IncomingLetterResponse, error) {
|
||||
return s.processor.CreateIncomingLetter(ctx, req)
|
||||
var result *contract.IncomingLetterResponse
|
||||
var recipients []entities.LetterIncomingRecipient
|
||||
|
||||
err := s.txManager.WithTransaction(ctx, func(txCtx context.Context) error {
|
||||
letterNumber, err := s.generateLetterNumber(txCtx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
req.LetterNumber = letterNumber
|
||||
|
||||
result, err = s.processor.CreateIncomingLetter(txCtx, req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
recipients, err = s.createDefaultRecipients(txCtx, result.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := s.createDispositionsForRecipients(txCtx, result.ID, recipients); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
s.logLetterCreation(txCtx, result.ID, letterNumber)
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Send notifications to all recipients after successful creation
|
||||
if s.notificationProcessor != nil && len(recipients) > 0 {
|
||||
go s.sendLetterNotifications(context.Background(), result, recipients)
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (s *LetterServiceImpl) generateLetterNumber(ctx context.Context) (string, error) {
|
||||
return s.numberGenerator.GenerateNumber(
|
||||
ctx,
|
||||
contract.SettingIncomingLetterPrefix,
|
||||
contract.SettingIncomingLetterSequence,
|
||||
DefaultIncomingLetterID,
|
||||
)
|
||||
}
|
||||
|
||||
func (s *LetterServiceImpl) createDefaultRecipients(ctx context.Context, letterID uuid.UUID) ([]entities.LetterIncomingRecipient, error) {
|
||||
return s.recipientProcessor.CreateDefaultRecipients(ctx, letterID)
|
||||
}
|
||||
|
||||
func (s *LetterServiceImpl) createDispositionsForRecipients(ctx context.Context, letterID uuid.UUID, recipients []entities.LetterIncomingRecipient) error {
|
||||
if len(recipients) == 0 || s.letterDispositionProcessor == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
departmentIDs := s.extractUniqueDepartmentIDs(recipients)
|
||||
if len(departmentIDs) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
systemDeptID := constant.SystemDepartmentID
|
||||
systemUserID := constant.SystemUserID
|
||||
|
||||
dispositionReq := &contract.CreateLetterDispositionRequest{
|
||||
FromDepartment: systemDeptID,
|
||||
LetterID: letterID,
|
||||
ToDepartmentIDs: departmentIDs,
|
||||
Notes: nil,
|
||||
CreatedBy: systemUserID,
|
||||
}
|
||||
|
||||
_, err := s.letterDispositionProcessor.CreateDispositions(ctx, dispositionReq)
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *LetterServiceImpl) extractUniqueDepartmentIDs(recipients []entities.LetterIncomingRecipient) []uuid.UUID {
|
||||
deptMap := make(map[uuid.UUID]bool)
|
||||
var departmentIDs []uuid.UUID
|
||||
|
||||
for _, recipient := range recipients {
|
||||
if recipient.RecipientDepartmentID != nil && !deptMap[*recipient.RecipientDepartmentID] {
|
||||
deptMap[*recipient.RecipientDepartmentID] = true
|
||||
departmentIDs = append(departmentIDs, *recipient.RecipientDepartmentID)
|
||||
}
|
||||
}
|
||||
|
||||
return departmentIDs
|
||||
}
|
||||
|
||||
func (s *LetterServiceImpl) logLetterCreation(ctx context.Context, letterID uuid.UUID, letterNumber string) {
|
||||
if s.activityLogger == nil {
|
||||
return
|
||||
}
|
||||
|
||||
userID := appcontext.FromGinContext(ctx).UserID
|
||||
err := s.activityLogger.LogLetterCreated(ctx, letterID, userID, letterNumber)
|
||||
if err != nil {
|
||||
logger.FromContext(ctx).Error("error when insert into log", err)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *LetterServiceImpl) addCreatorAsRecipient(ctx context.Context, letterID uuid.UUID, creatorID uuid.UUID) (*entities.LetterIncomingRecipient, error) {
|
||||
// Check if creator is already a recipient (to avoid duplicates)
|
||||
existingRecipients, err := s.processor.GetBatchRecipientsByUser(ctx, []uuid.UUID{letterID}, creatorID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// If creator is already a recipient, skip
|
||||
if _, exists := existingRecipients[letterID]; exists {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// Create recipient entry for the creator
|
||||
recipient := entities.LetterIncomingRecipient{
|
||||
ID: uuid.New(),
|
||||
LetterID: letterID,
|
||||
RecipientUserID: &creatorID,
|
||||
Status: entities.RecipientStatusNew,
|
||||
CreatedAt: time.Now(),
|
||||
}
|
||||
|
||||
// Save the recipient
|
||||
if err := s.recipientProcessor.CreateSingleRecipient(ctx, &recipient); err != nil {
|
||||
// Log error but don't fail the whole operation
|
||||
logger.FromContext(ctx).Error("failed to add creator as recipient", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &recipient, nil
|
||||
}
|
||||
|
||||
func (s *LetterServiceImpl) sendLetterNotifications(ctx context.Context, letter *contract.IncomingLetterResponse, recipients []entities.LetterIncomingRecipient) {
|
||||
for _, recipient := range recipients {
|
||||
// Only send notification to user recipients (not department recipients)
|
||||
// Also exclude the creator from receiving notifications
|
||||
if recipient.RecipientUserID != nil && *recipient.RecipientUserID != letter.CreatedBy {
|
||||
// Use description if available, otherwise use subject
|
||||
err := s.notificationProcessor.SendIncomingLetterNotification(
|
||||
ctx,
|
||||
letter.ID,
|
||||
*recipient.RecipientUserID,
|
||||
"Surat Masuk",
|
||||
letter.Subject)
|
||||
|
||||
if err != nil {
|
||||
// Log error but don't fail the entire operation
|
||||
logger.FromContext(ctx).Error("failed to send notification", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *LetterServiceImpl) GetIncomingLetterByID(ctx context.Context, id uuid.UUID) (*contract.IncomingLetterResponse, error) {
|
||||
return s.processor.GetIncomingLetterByID(ctx, id)
|
||||
}
|
||||
func (s *LetterServiceImpl) ListIncomingLetters(ctx context.Context, req *contract.ListIncomingLettersRequest) (*contract.ListIncomingLettersResponse, error) {
|
||||
return s.processor.ListIncomingLetters(ctx, req)
|
||||
appCtx := appcontext.FromGinContext(ctx)
|
||||
userID := appCtx.UserID
|
||||
departmentID := appCtx.DepartmentID
|
||||
|
||||
filter := repository.ListIncomingLettersFilter{
|
||||
Status: req.Status,
|
||||
Query: req.Query,
|
||||
DepartmentID: &departmentID,
|
||||
UserID: &userID,
|
||||
IsRead: req.IsRead,
|
||||
PriorityIDs: req.PriorityIDs,
|
||||
IsDispositioned: req.IsDispositioned,
|
||||
IsArchived: req.IsArchived,
|
||||
}
|
||||
|
||||
letters, total, err := s.processor.ListIncomingLetters(ctx, filter, req.Page, req.Limit)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(letters) == 0 {
|
||||
return &contract.ListIncomingLettersResponse{
|
||||
Letters: []contract.IncomingLetterResponse{},
|
||||
Pagination: transformer.CreatePaginationResponse(int(total), req.Page, req.Limit),
|
||||
TotalUnread: 0,
|
||||
}, nil
|
||||
}
|
||||
|
||||
letterIDs := make([]uuid.UUID, 0, len(letters))
|
||||
priorityIDSet := make(map[uuid.UUID]bool)
|
||||
institutionIDSet := make(map[uuid.UUID]bool)
|
||||
|
||||
for _, letter := range letters {
|
||||
letterIDs = append(letterIDs, letter.ID)
|
||||
if letter.PriorityID != nil {
|
||||
priorityIDSet[*letter.PriorityID] = true
|
||||
}
|
||||
if letter.SenderInstitutionID != nil {
|
||||
institutionIDSet[*letter.SenderInstitutionID] = true
|
||||
}
|
||||
}
|
||||
|
||||
priorityIDs := make([]uuid.UUID, 0, len(priorityIDSet))
|
||||
for id := range priorityIDSet {
|
||||
priorityIDs = append(priorityIDs, id)
|
||||
}
|
||||
|
||||
institutionIDs := make([]uuid.UUID, 0, len(institutionIDSet))
|
||||
for id := range institutionIDSet {
|
||||
institutionIDs = append(institutionIDs, id)
|
||||
}
|
||||
|
||||
type batchResult struct {
|
||||
attachments map[uuid.UUID][]entities.LetterIncomingAttachment
|
||||
priorities map[uuid.UUID]*entities.Priority
|
||||
institutions map[uuid.UUID]*entities.Institution
|
||||
recipients map[uuid.UUID]*entities.LetterIncomingRecipient
|
||||
err error
|
||||
}
|
||||
|
||||
resultChan := make(chan batchResult, 1)
|
||||
|
||||
go func() {
|
||||
result := batchResult{
|
||||
attachments: make(map[uuid.UUID][]entities.LetterIncomingAttachment),
|
||||
priorities: make(map[uuid.UUID]*entities.Priority),
|
||||
institutions: make(map[uuid.UUID]*entities.Institution),
|
||||
recipients: make(map[uuid.UUID]*entities.LetterIncomingRecipient),
|
||||
}
|
||||
|
||||
errChan := make(chan error, 4)
|
||||
|
||||
go func() {
|
||||
var err error
|
||||
result.attachments, err = s.processor.GetBatchAttachments(ctx, letterIDs)
|
||||
errChan <- err
|
||||
}()
|
||||
|
||||
go func() {
|
||||
var err error
|
||||
result.priorities, err = s.processor.GetBatchPriorities(ctx, priorityIDs)
|
||||
errChan <- err
|
||||
}()
|
||||
|
||||
go func() {
|
||||
var err error
|
||||
result.institutions, err = s.processor.GetBatchInstitutions(ctx, institutionIDs)
|
||||
errChan <- err
|
||||
}()
|
||||
|
||||
go func() {
|
||||
var err error
|
||||
result.recipients, err = s.processor.GetBatchRecipientsByUser(ctx, letterIDs, userID)
|
||||
errChan <- err
|
||||
}()
|
||||
|
||||
for i := 0; i < 4; i++ {
|
||||
if err := <-errChan; err != nil {
|
||||
logger.FromContext(ctx).Error("batch load error", err)
|
||||
}
|
||||
}
|
||||
|
||||
resultChan <- result
|
||||
}()
|
||||
|
||||
batchData := <-resultChan
|
||||
|
||||
respList := make([]contract.IncomingLetterResponse, 0, len(letters))
|
||||
for _, letter := range letters {
|
||||
attachments := batchData.attachments[letter.ID]
|
||||
if attachments == nil {
|
||||
attachments = []entities.LetterIncomingAttachment{}
|
||||
}
|
||||
|
||||
var priority *entities.Priority
|
||||
if letter.PriorityID != nil {
|
||||
priority = batchData.priorities[*letter.PriorityID]
|
||||
}
|
||||
|
||||
var institution *entities.Institution
|
||||
if letter.SenderInstitutionID != nil {
|
||||
institution = batchData.institutions[*letter.SenderInstitutionID]
|
||||
}
|
||||
|
||||
isRead := false
|
||||
if recipient, exists := batchData.recipients[letter.ID]; exists && recipient != nil {
|
||||
isRead = recipient.ReadAt != nil
|
||||
}
|
||||
|
||||
resp := transformer.LetterEntityToContract(&letter, attachments, priority, institution)
|
||||
resp.IsRead = isRead
|
||||
respList = append(respList, *resp)
|
||||
}
|
||||
|
||||
totalUnread, _ := s.processor.CountUnreadByUser(ctx, userID)
|
||||
|
||||
return &contract.ListIncomingLettersResponse{
|
||||
Letters: respList,
|
||||
Pagination: transformer.CreatePaginationResponse(int(total), req.Page, req.Limit),
|
||||
TotalUnread: totalUnread,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *LetterServiceImpl) GetLetterUnreadCounts(ctx context.Context) (*contract.LetterUnreadCountResponse, error) {
|
||||
return s.processor.GetLetterUnreadCounts(ctx)
|
||||
}
|
||||
|
||||
func (s *LetterServiceImpl) MarkIncomingLetterAsRead(ctx context.Context, letterID uuid.UUID) (*contract.MarkLetterReadResponse, error) {
|
||||
return s.processor.MarkIncomingLetterAsRead(ctx, letterID)
|
||||
}
|
||||
func (s *LetterServiceImpl) MarkOutgoingLetterAsRead(ctx context.Context, letterID uuid.UUID) (*contract.MarkLetterReadResponse, error) {
|
||||
return s.processor.MarkOutgoingLetterAsRead(ctx, letterID)
|
||||
}
|
||||
func (s *LetterServiceImpl) UpdateIncomingLetter(ctx context.Context, id uuid.UUID, req *contract.UpdateIncomingLetterRequest) (*contract.IncomingLetterResponse, error) {
|
||||
return s.processor.UpdateIncomingLetter(ctx, id, req)
|
||||
@@ -47,7 +423,25 @@ func (s *LetterServiceImpl) SoftDeleteIncomingLetter(ctx context.Context, id uui
|
||||
}
|
||||
|
||||
func (s *LetterServiceImpl) CreateDispositions(ctx context.Context, req *contract.CreateLetterDispositionRequest) (*contract.ListDispositionsResponse, error) {
|
||||
return s.processor.CreateDispositions(ctx, req)
|
||||
userID := appcontext.FromGinContext(ctx).UserID
|
||||
req.CreatedBy = userID
|
||||
|
||||
if req.FromDepartment == uuid.Nil {
|
||||
req.FromDepartment = appcontext.FromGinContext(ctx).DepartmentID
|
||||
}
|
||||
|
||||
var result *contract.ListDispositionsResponse
|
||||
err := s.txManager.WithTransaction(ctx, func(txCtx context.Context) error {
|
||||
var err error
|
||||
result, err = s.processor.CreateDispositions(txCtx, req)
|
||||
return err
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (s *LetterServiceImpl) GetEnhancedDispositionsByLetter(ctx context.Context, letterID uuid.UUID) (*contract.ListEnhancedDispositionsResponse, error) {
|
||||
@@ -61,3 +455,36 @@ func (s *LetterServiceImpl) CreateDiscussion(ctx context.Context, letterID uuid.
|
||||
func (s *LetterServiceImpl) UpdateDiscussion(ctx context.Context, letterID uuid.UUID, discussionID uuid.UUID, req *contract.UpdateLetterDiscussionRequest) (*contract.LetterDiscussionResponse, error) {
|
||||
return s.processor.UpdateDiscussion(ctx, letterID, discussionID, req)
|
||||
}
|
||||
|
||||
func (s *LetterServiceImpl) GetDepartmentDispositionStatus(ctx context.Context, req *contract.GetDepartmentDispositionStatusRequest) (*contract.ListDepartmentDispositionStatusResponse, error) {
|
||||
return s.processor.GetDepartmentDispositionStatus(ctx, req)
|
||||
}
|
||||
|
||||
func (s *LetterServiceImpl) UpdateDispositionStatus(ctx context.Context, req *contract.UpdateDispositionStatusRequest) (*contract.DepartmentDispositionStatusResponse, error) {
|
||||
// For now, delegate to the processor which handles this
|
||||
// The processor needs to be refactored to remove context extraction
|
||||
return s.processor.UpdateDispositionStatus(ctx, req)
|
||||
}
|
||||
|
||||
func (s *LetterServiceImpl) GetLetterCTA(ctx context.Context, letterID uuid.UUID) (*contract.LetterCTAResponse, error) {
|
||||
departmentID := appcontext.FromGinContext(ctx).DepartmentID
|
||||
return s.processor.GetLetterCTA(ctx, letterID, departmentID)
|
||||
}
|
||||
|
||||
func (s *LetterServiceImpl) BulkArchiveIncomingLetters(ctx context.Context, letterIDs []uuid.UUID) (*contract.BulkArchiveLettersResponse, error) {
|
||||
// Extract user context to archive only for the current user
|
||||
appCtx := appcontext.FromGinContext(ctx)
|
||||
userID := appCtx.UserID
|
||||
|
||||
// Archive letters only for the current user
|
||||
archivedCount, err := s.processor.BulkArchiveIncomingLettersForUser(ctx, letterIDs, userID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &contract.BulkArchiveLettersResponse{
|
||||
Success: true,
|
||||
Message: "Letters archived successfully",
|
||||
ArchivedCount: int(archivedCount),
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -0,0 +1,385 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/url"
|
||||
|
||||
"eslogad-be/internal/config"
|
||||
"eslogad-be/internal/contract"
|
||||
|
||||
"github.com/google/uuid"
|
||||
novu "github.com/novuhq/go-novu/lib"
|
||||
)
|
||||
|
||||
type NotificationService interface {
|
||||
TriggerNotification(ctx context.Context, req *contract.TriggerNotificationRequest) (*contract.TriggerNotificationResponse, error)
|
||||
BulkTriggerNotification(ctx context.Context, req *contract.BulkTriggerNotificationRequest) (*contract.BulkTriggerNotificationResponse, error)
|
||||
GetSubscriber(ctx context.Context, userID uuid.UUID) (*contract.GetSubscriberResponse, error)
|
||||
UpdateSubscriberChannel(ctx context.Context, req *contract.UpdateSubscriberChannelRequest) (*contract.UpdateSubscriberChannelResponse, error)
|
||||
}
|
||||
|
||||
type NotificationServiceImpl struct {
|
||||
client *novu.APIClient
|
||||
config *config.NovuConfig
|
||||
userProcessor UserProcessorForNotification
|
||||
}
|
||||
|
||||
type UserProcessorForNotification interface {
|
||||
GetUserByID(ctx context.Context, id uuid.UUID) (*contract.UserResponse, error)
|
||||
}
|
||||
|
||||
func NewNotificationService(cfg *config.NovuConfig, userProcessor UserProcessorForNotification) *NotificationServiceImpl {
|
||||
var client *novu.APIClient
|
||||
if cfg.APIKey != "" {
|
||||
// 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 &NotificationServiceImpl{
|
||||
client: client,
|
||||
config: cfg,
|
||||
userProcessor: userProcessor,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *NotificationServiceImpl) TriggerNotification(ctx context.Context, req *contract.TriggerNotificationRequest) (*contract.TriggerNotificationResponse, error) {
|
||||
if s.client == nil {
|
||||
return &contract.TriggerNotificationResponse{
|
||||
Success: false,
|
||||
Message: "notification service not configured",
|
||||
}, nil
|
||||
}
|
||||
|
||||
subscriberID := req.UserID.String()
|
||||
|
||||
_, err := s.ensureSubscriberExists(ctx, req.UserID)
|
||||
if err != nil {
|
||||
return &contract.TriggerNotificationResponse{
|
||||
Success: false,
|
||||
Message: fmt.Sprintf("failed to ensure subscriber exists: %v", err),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Prepare the trigger payload
|
||||
to := map[string]interface{}{
|
||||
"subscriberId": subscriberID,
|
||||
}
|
||||
|
||||
// Add additional recipient information if provided
|
||||
if req.To != nil {
|
||||
if req.To.Email != "" {
|
||||
to["email"] = req.To.Email
|
||||
}
|
||||
if req.To.Phone != "" {
|
||||
to["phone"] = req.To.Phone
|
||||
}
|
||||
}
|
||||
|
||||
// Prepare overrides
|
||||
overrides := make(map[string]interface{})
|
||||
if req.Overrides != nil {
|
||||
if req.Overrides.Email != nil {
|
||||
overrides["email"] = req.Overrides.Email
|
||||
}
|
||||
if req.Overrides.SMS != nil {
|
||||
overrides["sms"] = req.Overrides.SMS
|
||||
}
|
||||
if req.Overrides.InApp != nil {
|
||||
overrides["in_app"] = req.Overrides.InApp
|
||||
}
|
||||
if req.Overrides.Push != nil {
|
||||
overrides["push"] = req.Overrides.Push
|
||||
}
|
||||
if req.Overrides.Chat != nil {
|
||||
overrides["chat"] = req.Overrides.Chat
|
||||
}
|
||||
}
|
||||
|
||||
// Trigger the notification using the template ID
|
||||
triggerPayload := novu.ITriggerPayloadOptions{
|
||||
To: to,
|
||||
Payload: req.TemplateData,
|
||||
Overrides: overrides,
|
||||
}
|
||||
|
||||
resp, err := s.client.EventApi.Trigger(ctx, req.TemplateID, triggerPayload)
|
||||
if err != nil {
|
||||
return &contract.TriggerNotificationResponse{
|
||||
Success: false,
|
||||
Message: fmt.Sprintf("failed to trigger notification: %v", err),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Extract transaction ID from response
|
||||
transactionID := ""
|
||||
if respData, ok := resp.Data.(map[string]interface{}); ok {
|
||||
if txID, exists := respData["transactionId"]; exists {
|
||||
transactionID = fmt.Sprintf("%v", txID)
|
||||
}
|
||||
}
|
||||
|
||||
return &contract.TriggerNotificationResponse{
|
||||
Success: true,
|
||||
TransactionID: transactionID,
|
||||
Message: "notification triggered successfully",
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *NotificationServiceImpl) BulkTriggerNotification(ctx context.Context, req *contract.BulkTriggerNotificationRequest) (*contract.BulkTriggerNotificationResponse, error) {
|
||||
if s.client == nil {
|
||||
return &contract.BulkTriggerNotificationResponse{
|
||||
Success: false,
|
||||
TotalSent: 0,
|
||||
TotalFailed: len(req.UserIDs),
|
||||
}, nil
|
||||
}
|
||||
|
||||
results := make([]contract.NotificationResult, 0, len(req.UserIDs))
|
||||
successCount := 0
|
||||
failedCount := 0
|
||||
|
||||
for _, userID := range req.UserIDs {
|
||||
// Create individual trigger request
|
||||
triggerReq := &contract.TriggerNotificationRequest{
|
||||
UserID: userID,
|
||||
TemplateID: req.TemplateID,
|
||||
TemplateData: req.TemplateData,
|
||||
Overrides: req.Overrides,
|
||||
}
|
||||
|
||||
resp, err := s.TriggerNotification(ctx, triggerReq)
|
||||
|
||||
result := contract.NotificationResult{
|
||||
UserID: userID,
|
||||
Success: resp.Success,
|
||||
}
|
||||
|
||||
if resp.Success {
|
||||
result.TransactionID = resp.TransactionID
|
||||
successCount++
|
||||
} else {
|
||||
if err != nil {
|
||||
result.Error = err.Error()
|
||||
} else {
|
||||
result.Error = resp.Message
|
||||
}
|
||||
failedCount++
|
||||
}
|
||||
|
||||
results = append(results, result)
|
||||
}
|
||||
|
||||
return &contract.BulkTriggerNotificationResponse{
|
||||
Success: failedCount == 0,
|
||||
TotalSent: successCount,
|
||||
TotalFailed: failedCount,
|
||||
Results: results,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *NotificationServiceImpl) GetSubscriber(ctx context.Context, userID uuid.UUID) (*contract.GetSubscriberResponse, error) {
|
||||
if s.client == nil {
|
||||
return nil, fmt.Errorf("notification service not configured")
|
||||
}
|
||||
|
||||
subscriberID := userID.String()
|
||||
|
||||
// Try to get the subscriber
|
||||
subscriber, err := s.client.SubscriberApi.Get(ctx, subscriberID)
|
||||
if err != nil {
|
||||
// If subscriber doesn't exist, create it
|
||||
_, createErr := s.ensureSubscriberExists(ctx, userID)
|
||||
if createErr != nil {
|
||||
return nil, fmt.Errorf("failed to get or create subscriber: %w", createErr)
|
||||
}
|
||||
|
||||
// Try to get again after creation
|
||||
subscriber, err = s.client.SubscriberApi.Get(ctx, subscriberID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get subscriber after creation: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Convert Novu subscriber to our response format
|
||||
response := &contract.GetSubscriberResponse{
|
||||
SubscriberID: subscriberID,
|
||||
}
|
||||
|
||||
if subData, ok := subscriber.Data.(map[string]interface{}); ok {
|
||||
if email, exists := subData["email"]; exists {
|
||||
response.Email = fmt.Sprintf("%v", email)
|
||||
}
|
||||
if firstName, exists := subData["firstName"]; exists {
|
||||
response.FirstName = fmt.Sprintf("%v", firstName)
|
||||
}
|
||||
if lastName, exists := subData["lastName"]; exists {
|
||||
response.LastName = fmt.Sprintf("%v", lastName)
|
||||
}
|
||||
if phone, exists := subData["phone"]; exists {
|
||||
response.Phone = fmt.Sprintf("%v", phone)
|
||||
}
|
||||
if avatar, exists := subData["avatar"]; exists {
|
||||
response.Avatar = fmt.Sprintf("%v", avatar)
|
||||
}
|
||||
if data, exists := subData["data"]; exists {
|
||||
if dataMap, ok := data.(map[string]interface{}); ok {
|
||||
response.Data = dataMap
|
||||
}
|
||||
}
|
||||
if channels, exists := subData["channels"]; exists {
|
||||
if channelList, ok := channels.([]interface{}); ok {
|
||||
response.Channels = make([]contract.ChannelCredentials, 0, len(channelList))
|
||||
for _, ch := range channelList {
|
||||
if chMap, ok := ch.(map[string]interface{}); ok {
|
||||
channelCred := contract.ChannelCredentials{}
|
||||
if chType, exists := chMap["providerId"]; exists {
|
||||
channelCred.Channel = fmt.Sprintf("%v", chType)
|
||||
}
|
||||
if creds, exists := chMap["credentials"]; exists {
|
||||
if credMap, ok := creds.(map[string]interface{}); ok {
|
||||
channelCred.Credentials = credMap
|
||||
}
|
||||
}
|
||||
response.Channels = append(response.Channels, channelCred)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return response, nil
|
||||
}
|
||||
|
||||
func (s *NotificationServiceImpl) UpdateSubscriberChannel(ctx context.Context, req *contract.UpdateSubscriberChannelRequest) (*contract.UpdateSubscriberChannelResponse, error) {
|
||||
if s.client == nil {
|
||||
return &contract.UpdateSubscriberChannelResponse{
|
||||
Success: false,
|
||||
Message: "notification service not configured",
|
||||
}, nil
|
||||
}
|
||||
|
||||
subscriberID := req.UserID.String()
|
||||
|
||||
// Ensure subscriber exists
|
||||
_, err := s.ensureSubscriberExists(ctx, req.UserID)
|
||||
if err != nil {
|
||||
return &contract.UpdateSubscriberChannelResponse{
|
||||
Success: false,
|
||||
Message: fmt.Sprintf("failed to ensure subscriber exists: %v", err),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Since the Novu Go SDK doesn't have UpdateCredentials, we'll update the subscriber data instead
|
||||
// Get user info to update subscriber
|
||||
user, err := s.userProcessor.GetUserByID(ctx, req.UserID)
|
||||
if err != nil {
|
||||
return &contract.UpdateSubscriberChannelResponse{
|
||||
Success: false,
|
||||
Message: fmt.Sprintf("failed to get user data: %v", err),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Prepare subscriber data with channel credentials stored in data
|
||||
data := map[string]interface{}{
|
||||
"userId": user.ID.String(),
|
||||
"email": user.Email,
|
||||
"isActive": user.IsActive,
|
||||
"updatedAt": user.UpdatedAt,
|
||||
}
|
||||
|
||||
// Store channel credentials in the subscriber data
|
||||
channelKey := fmt.Sprintf("channel_%s", req.Channel)
|
||||
data[channelKey] = req.Credentials
|
||||
|
||||
// Update subscriber with new data
|
||||
updateData := novu.SubscriberPayload{
|
||||
Email: user.Email,
|
||||
FirstName: user.Name,
|
||||
LastName: "",
|
||||
Phone: "",
|
||||
Avatar: "",
|
||||
Data: data,
|
||||
}
|
||||
|
||||
_, err = s.client.SubscriberApi.Update(ctx, subscriberID, updateData)
|
||||
if err != nil {
|
||||
return &contract.UpdateSubscriberChannelResponse{
|
||||
Success: false,
|
||||
Message: fmt.Sprintf("failed to update subscriber channel: %v", err),
|
||||
}, nil
|
||||
}
|
||||
|
||||
return &contract.UpdateSubscriberChannelResponse{
|
||||
Success: true,
|
||||
Message: "subscriber channel updated successfully",
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *NotificationServiceImpl) ensureSubscriberExists(ctx context.Context, userID uuid.UUID) (bool, error) {
|
||||
subscriberID := userID.String()
|
||||
|
||||
_, err := s.client.SubscriberApi.Get(ctx, subscriberID)
|
||||
if err == nil {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
user, err := s.userProcessor.GetUserByID(ctx, userID)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("failed to get user data: %w", err)
|
||||
}
|
||||
|
||||
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 = s.client.SubscriberApi.Identify(ctx, subscriberID, subscriber)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("failed to create subscriber: %w", err)
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}
|
||||
@@ -13,6 +13,7 @@ type UserProcessor interface {
|
||||
CreateUser(ctx context.Context, req *contract.CreateUserRequest) (*contract.UserResponse, error)
|
||||
DeleteUser(ctx context.Context, id uuid.UUID) error
|
||||
GetUserByID(ctx context.Context, id uuid.UUID) (*contract.UserResponse, error)
|
||||
GetUserByIDLight(ctx context.Context, id uuid.UUID) (*contract.UserResponse, error)
|
||||
GetUserByEmail(ctx context.Context, email string) (*contract.UserResponse, error)
|
||||
GetUserEntityByEmail(ctx context.Context, email string) (*entities.User, error)
|
||||
ChangePassword(ctx context.Context, userID uuid.UUID, req *contract.ChangePasswordRequest) error
|
||||
|
||||
Reference in New Issue
Block a user