diff --git a/internal/app/app.go b/internal/app/app.go index 6ff1f5d..d6f780c 100644 --- a/internal/app/app.go +++ b/internal/app/app.go @@ -131,64 +131,96 @@ type repositories struct { departmentRepo *repository.DepartmentRepository userDeptRepo *repository.UserDepartmentRepository // letter outgoing repos - letterOutgoingRepo *repository.LetterOutgoingRepository - letterOutgoingAttachmentRepo *repository.LetterOutgoingAttachmentRepository - letterOutgoingRecipientRepo *repository.LetterOutgoingRecipientRepository - letterOutgoingDiscussionRepo *repository.LetterOutgoingDiscussionRepository - letterOutgoingDiscussionAttachRepo *repository.LetterOutgoingDiscussionAttachmentRepository - letterOutgoingActivityLogRepo *repository.LetterOutgoingActivityLogRepository - approvalFlowRepo *repository.ApprovalFlowRepository - letterOutgoingApprovalRepo *repository.LetterOutgoingApprovalRepository + letterOutgoingRepo *repository.LetterOutgoingRepository + letterOutgoingAttachmentRepo *repository.LetterOutgoingAttachmentRepository + letterOutgoingRecipientRepo *repository.LetterOutgoingRecipientRepository + letterOutgoingDiscussionRepo *repository.LetterOutgoingDiscussionRepository + letterOutgoingDiscussionAttachRepo *repository.LetterOutgoingDiscussionAttachmentRepository + letterOutgoingActivityLogRepo *repository.LetterOutgoingActivityLogRepository + approvalFlowRepo *repository.ApprovalFlowRepository + letterOutgoingApprovalRepo *repository.LetterOutgoingApprovalRepository } func (a *App) initRepositories() *repositories { return &repositories{ - userRepo: repository.NewUserRepository(a.db), - userProfileRepo: repository.NewUserProfileRepository(a.db), - titleRepo: repository.NewTitleRepository(a.db), - rbacRepo: repository.NewRBACRepository(a.db), - labelRepo: repository.NewLabelRepository(a.db), - priorityRepo: repository.NewPriorityRepository(a.db), - institutionRepo: repository.NewInstitutionRepository(a.db), - dispRepo: repository.NewDispositionActionRepository(a.db), - letterRepo: repository.NewLetterIncomingRepository(a.db), - letterAttachRepo: repository.NewLetterIncomingAttachmentRepository(a.db), - activityLogRepo: repository.NewLetterIncomingActivityLogRepository(a.db), - dispositionRouteRepo: repository.NewDispositionRouteRepository(a.db), - letterDispositionRepo: repository.NewLetterIncomingDispositionRepository(a.db), - letterDispositionDeptRepo: repository.NewLetterIncomingDispositionDepartmentRepository(a.db), - letterDispActionSelRepo: repository.NewLetterDispositionActionSelectionRepository(a.db), - dispositionNoteRepo: repository.NewDispositionNoteRepository(a.db), - letterDiscussionRepo: repository.NewLetterDiscussionRepository(a.db), - settingRepo: repository.NewAppSettingRepository(a.db), - recipientRepo: repository.NewLetterIncomingRecipientRepository(a.db), - departmentRepo: repository.NewDepartmentRepository(a.db), - userDeptRepo: repository.NewUserDepartmentRepository(a.db), - // letter outgoing repos - letterOutgoingRepo: repository.NewLetterOutgoingRepository(a.db), - letterOutgoingAttachmentRepo: repository.NewLetterOutgoingAttachmentRepository(a.db), - letterOutgoingRecipientRepo: repository.NewLetterOutgoingRecipientRepository(a.db), - letterOutgoingDiscussionRepo: repository.NewLetterOutgoingDiscussionRepository(a.db), - letterOutgoingDiscussionAttachRepo: repository.NewLetterOutgoingDiscussionAttachmentRepository(a.db), - letterOutgoingActivityLogRepo: repository.NewLetterOutgoingActivityLogRepository(a.db), - approvalFlowRepo: repository.NewApprovalFlowRepository(a.db), - letterOutgoingApprovalRepo: repository.NewLetterOutgoingApprovalRepository(a.db), + userRepo: repository.NewUserRepository(a.db), + userProfileRepo: repository.NewUserProfileRepository(a.db), + titleRepo: repository.NewTitleRepository(a.db), + rbacRepo: repository.NewRBACRepository(a.db), + labelRepo: repository.NewLabelRepository(a.db), + priorityRepo: repository.NewPriorityRepository(a.db), + institutionRepo: repository.NewInstitutionRepository(a.db), + dispRepo: repository.NewDispositionActionRepository(a.db), + letterRepo: repository.NewLetterIncomingRepository(a.db), + letterAttachRepo: repository.NewLetterIncomingAttachmentRepository(a.db), + activityLogRepo: repository.NewLetterIncomingActivityLogRepository(a.db), + dispositionRouteRepo: repository.NewDispositionRouteRepository(a.db), + letterDispositionRepo: repository.NewLetterIncomingDispositionRepository(a.db), + letterDispositionDeptRepo: repository.NewLetterIncomingDispositionDepartmentRepository(a.db), + letterDispActionSelRepo: repository.NewLetterDispositionActionSelectionRepository(a.db), + dispositionNoteRepo: repository.NewDispositionNoteRepository(a.db), + letterDiscussionRepo: repository.NewLetterDiscussionRepository(a.db), + settingRepo: repository.NewAppSettingRepository(a.db), + recipientRepo: repository.NewLetterIncomingRecipientRepository(a.db), + departmentRepo: repository.NewDepartmentRepository(a.db), + userDeptRepo: repository.NewUserDepartmentRepository(a.db), + letterOutgoingRepo: repository.NewLetterOutgoingRepository(a.db), + letterOutgoingAttachmentRepo: repository.NewLetterOutgoingAttachmentRepository(a.db), + letterOutgoingRecipientRepo: repository.NewLetterOutgoingRecipientRepository(a.db), + letterOutgoingDiscussionRepo: repository.NewLetterOutgoingDiscussionRepository(a.db), + letterOutgoingDiscussionAttachRepo: repository.NewLetterOutgoingDiscussionAttachmentRepository(a.db), + letterOutgoingActivityLogRepo: repository.NewLetterOutgoingActivityLogRepository(a.db), + approvalFlowRepo: repository.NewApprovalFlowRepository(a.db), + letterOutgoingApprovalRepo: repository.NewLetterOutgoingApprovalRepository(a.db), } } type processors struct { - userProcessor *processor.UserProcessorImpl - letterProcessor *processor.LetterProcessorImpl - activityLogger *processor.ActivityLogProcessorImpl + userProcessor *processor.UserProcessorImpl + letterProcessor *processor.LetterProcessorImpl + letterOutgoingProcessor *processor.LetterOutgoingProcessorImpl + activityLogger *processor.ActivityLogProcessorImpl + letterNumberGenerator *processor.LetterNumberGeneratorImpl } func (a *App) initProcessors(cfg *config.Config, repos *repositories) *processors { txMgr := repository.NewTxManager(a.db) activity := processor.NewActivityLogProcessor(repos.activityLogRepo) + + // Create the letter number generator + letterNumberGen := processor.NewLetterNumberGenerator(repos.settingRepo) + + // Create letter processors with the number generator + letterProc := processor.NewLetterProcessor( + repos.letterRepo, repos.letterAttachRepo, txMgr, activity, + repos.letterDispositionRepo, repos.letterDispositionDeptRepo, + repos.letterDispActionSelRepo, repos.dispositionNoteRepo, + repos.letterDiscussionRepo, repos.settingRepo, + repos.recipientRepo, repos.departmentRepo, repos.userDeptRepo, + repos.priorityRepo, repos.institutionRepo, repos.dispRepo, + letterNumberGen, + ) + + letterOutgoingProc := processor.NewLetterOutgoingProcessor( + a.db, + repos.letterOutgoingRepo, + repos.letterOutgoingAttachmentRepo, + repos.letterOutgoingRecipientRepo, + repos.letterOutgoingDiscussionRepo, + repos.letterOutgoingDiscussionAttachRepo, + repos.letterOutgoingActivityLogRepo, + repos.approvalFlowRepo, + repos.letterOutgoingApprovalRepo, + letterNumberGen, + txMgr, + ) + return &processors{ - userProcessor: processor.NewUserProcessor(repos.userRepo, repos.userProfileRepo), - letterProcessor: processor.NewLetterProcessor(repos.letterRepo, repos.letterAttachRepo, txMgr, activity, repos.letterDispositionRepo, repos.letterDispositionDeptRepo, repos.letterDispActionSelRepo, repos.dispositionNoteRepo, repos.letterDiscussionRepo, repos.settingRepo, repos.recipientRepo, repos.departmentRepo, repos.userDeptRepo, repos.priorityRepo, repos.institutionRepo, repos.dispRepo), - activityLogger: activity, + userProcessor: processor.NewUserProcessor(repos.userRepo, repos.userProfileRepo), + letterProcessor: letterProc, + letterOutgoingProcessor: letterOutgoingProc, + activityLogger: activity, + letterNumberGenerator: letterNumberGen, } } @@ -217,25 +249,14 @@ func (a *App) initServices(processors *processors, repos *repositories, cfg *con rbacSvc := service.NewRBACService(repos.rbacRepo) - masterSvc := service.NewMasterService(repos.labelRepo, repos.priorityRepo, repos.institutionRepo, repos.dispRepo) + masterSvc := service.NewMasterService(repos.labelRepo, repos.priorityRepo, repos.institutionRepo, repos.dispRepo, repos.departmentRepo) letterSvc := service.NewLetterService(processors.letterProcessor) dispRouteSvc := service.NewDispositionRouteService(repos.dispositionRouteRepo) - + txManager := repository.NewTxManager(a.db) - letterOutgoingSvc := service.NewLetterOutgoingService( - a.db, - repos.letterOutgoingRepo, - repos.letterOutgoingAttachmentRepo, - repos.letterOutgoingRecipientRepo, - repos.letterOutgoingDiscussionRepo, - repos.letterOutgoingDiscussionAttachRepo, - repos.letterOutgoingActivityLogRepo, - repos.approvalFlowRepo, - repos.letterOutgoingApprovalRepo, - txManager, - ) - + letterOutgoingSvc := service.NewLetterOutgoingService(processors.letterOutgoingProcessor) + approvalFlowStepRepo := repository.NewApprovalFlowStepRepository(a.db) approvalFlowSvc := service.NewApprovalFlowService( a.db, diff --git a/internal/contract/common.go b/internal/contract/common.go index b8963dc..a4dcbb7 100644 --- a/internal/contract/common.go +++ b/internal/contract/common.go @@ -6,6 +6,8 @@ const ( SettingIncomingLetterPrefix = "INCOMING_LETTER_PREFIX" SettingIncomingLetterSequence = "INCOMING_LETTER_SEQUENCE" SettingIncomingLetterRecipients = "INCOMING_LETTER_RECIPIENTS" + SettingOutgoingLetterPrefix = "OUTGOING_LETTER_PREFIX" + SettingOutgoingLetterSequence = "OUTGOING_LETTER_SEQUENCE" ) type ErrorResponse struct { diff --git a/internal/contract/letter_outgoing_contract.go b/internal/contract/letter_outgoing_contract.go index 5feebe6..d3d4f68 100644 --- a/internal/contract/letter_outgoing_contract.go +++ b/internal/contract/letter_outgoing_contract.go @@ -27,8 +27,6 @@ type CreateOutgoingLetterRequest struct { PriorityID *uuid.UUID `json:"priority_id,omitempty"` ReceiverInstitutionID *uuid.UUID `json:"receiver_institution_id,omitempty"` IssueDate time.Time `json:"issue_date" validate:"required"` - ApprovalFlowID *uuid.UUID `json:"approval_flow_id,omitempty"` - Recipients []CreateOutgoingLetterRecipient `json:"recipients,omitempty"` Attachments []CreateOutgoingLetterAttachment `json:"attachments,omitempty"` UserID uuid.UUID } diff --git a/internal/contract/user_contract.go b/internal/contract/user_contract.go index 2ebb6de..a14a4f2 100644 --- a/internal/contract/user_contract.go +++ b/internal/contract/user_contract.go @@ -86,6 +86,19 @@ type DepartmentResponse struct { Path string `json:"path"` } +type ListDepartmentsRequest struct { + Search string `json:"search,omitempty" form:"search"` + Page int `json:"page,omitempty" form:"page"` + Limit int `json:"limit,omitempty" form:"limit"` +} + +type ListDepartmentsResponse struct { + Departments []DepartmentResponse `json:"departments"` + Total int64 `json:"total"` + Page int `json:"page"` + Limit int `json:"limit"` +} + type UserProfileResponse struct { UserID uuid.UUID `json:"user_id"` FullName string `json:"full_name"` diff --git a/internal/handler/letter_handler.go b/internal/handler/letter_handler.go index b739759..1e4827d 100644 --- a/internal/handler/letter_handler.go +++ b/internal/handler/letter_handler.go @@ -26,161 +26,241 @@ type LetterService interface { UpdateDiscussion(ctx context.Context, letterID uuid.UUID, discussionID uuid.UUID, req *contract.UpdateLetterDiscussionRequest) (*contract.LetterDiscussionResponse, error) } -type LetterHandler struct{ svc LetterService } +type LetterHandler struct { + svc LetterService +} -func NewLetterHandler(svc LetterService) *LetterHandler { return &LetterHandler{svc: svc} } +func NewLetterHandler(svc LetterService) *LetterHandler { + return &LetterHandler{svc: svc} +} + +// Helper functions for common patterns +func (h *LetterHandler) parseUUID(c *gin.Context, param string) (uuid.UUID, bool) { + id, err := uuid.Parse(c.Param(param)) + if err != nil { + h.respondError(c, http.StatusBadRequest, "invalid "+param) + return uuid.Nil, false + } + return id, true +} + +func (h *LetterHandler) bindJSON(c *gin.Context, req interface{}) bool { + if err := c.ShouldBindJSON(req); err != nil { + h.respondError(c, http.StatusBadRequest, "invalid request body") + return false + } + return true +} + +func (h *LetterHandler) bindQuery(c *gin.Context, req interface{}) bool { + if err := c.ShouldBindQuery(req); err != nil { + h.respondError(c, http.StatusBadRequest, "invalid query parameters") + return false + } + return true +} + +func (h *LetterHandler) respondError(c *gin.Context, code int, message string) { + c.JSON(code, &contract.ErrorResponse{ + Error: message, + Code: code, + }) +} + +func (h *LetterHandler) respondSuccess(c *gin.Context, code int, data interface{}) { + c.JSON(code, contract.BuildSuccessResponse(data)) +} + +func (h *LetterHandler) handleServiceError(c *gin.Context, err error) { + if err != nil { + h.respondError(c, http.StatusInternalServerError, err.Error()) + } +} func (h *LetterHandler) CreateIncomingLetter(c *gin.Context) { var req contract.CreateIncomingLetterRequest - if err := c.ShouldBindJSON(&req); err != nil { - c.JSON(http.StatusBadRequest, &contract.ErrorResponse{Error: "invalid body", Code: http.StatusBadRequest}) + if !h.bindJSON(c, &req) { return } + resp, err := h.svc.CreateIncomingLetter(c.Request.Context(), &req) if err != nil { - c.JSON(http.StatusInternalServerError, &contract.ErrorResponse{Error: err.Error(), Code: 500}) + h.handleServiceError(c, err) return } - c.JSON(http.StatusCreated, contract.BuildSuccessResponse(resp)) + + h.respondSuccess(c, http.StatusCreated, resp) } func (h *LetterHandler) GetIncomingLetter(c *gin.Context) { - id, err := uuid.Parse(c.Param("id")) - if err != nil { - c.JSON(400, &contract.ErrorResponse{Error: "invalid id", Code: 400}) + id, ok := h.parseUUID(c, "id") + if !ok { return } + resp, err := h.svc.GetIncomingLetterByID(c.Request.Context(), id) if err != nil { - c.JSON(500, &contract.ErrorResponse{Error: err.Error(), Code: 500}) + h.handleServiceError(c, err) return } - c.JSON(200, contract.BuildSuccessResponse(resp)) + + h.respondSuccess(c, http.StatusOK, resp) } func (h *LetterHandler) ListIncomingLetters(c *gin.Context) { - page, _ := strconv.Atoi(c.DefaultQuery("page", "1")) - limit, _ := strconv.Atoi(c.DefaultQuery("limit", "10")) - status := c.Query("status") - query := c.Query("q") - var statusPtr *string - var queryPtr *string - if status != "" { - statusPtr = &status - } - if query != "" { - queryPtr = &query - } - req := &contract.ListIncomingLettersRequest{Page: page, Limit: limit, Status: statusPtr, Query: queryPtr} + req := h.parseListRequest(c) + resp, err := h.svc.ListIncomingLetters(c.Request.Context(), req) if err != nil { - c.JSON(500, &contract.ErrorResponse{Error: err.Error(), Code: 500}) + h.handleServiceError(c, err) return } - c.JSON(200, contract.BuildSuccessResponse(resp)) + + h.respondSuccess(c, http.StatusOK, resp) +} + +func (h *LetterHandler) parseListRequest(c *gin.Context) *contract.ListIncomingLettersRequest { + page, _ := strconv.Atoi(c.DefaultQuery("page", "1")) + limit, _ := strconv.Atoi(c.DefaultQuery("limit", "10")) + + // Ensure valid pagination values + if page < 1 { + page = 1 + } + if limit < 1 { + limit = 10 + } + if limit > 100 { + limit = 100 + } + + req := &contract.ListIncomingLettersRequest{ + Page: page, + Limit: limit, + } + + // Handle optional query parameters + if status := c.Query("status"); status != "" { + req.Status = &status + } + if query := c.Query("q"); query != "" { + req.Query = &query + } + + return req } func (h *LetterHandler) UpdateIncomingLetter(c *gin.Context) { - id, err := uuid.Parse(c.Param("id")) - if err != nil { - c.JSON(400, &contract.ErrorResponse{Error: "invalid id", Code: 400}) + id, ok := h.parseUUID(c, "id") + if !ok { return } + var req contract.UpdateIncomingLetterRequest - if err := c.ShouldBindJSON(&req); err != nil { - c.JSON(400, &contract.ErrorResponse{Error: "invalid body", Code: 400}) + if !h.bindJSON(c, &req) { return } + resp, err := h.svc.UpdateIncomingLetter(c.Request.Context(), id, &req) if err != nil { - c.JSON(500, &contract.ErrorResponse{Error: err.Error(), Code: 500}) + h.handleServiceError(c, err) return } - c.JSON(200, contract.BuildSuccessResponse(resp)) + + h.respondSuccess(c, http.StatusOK, resp) } func (h *LetterHandler) DeleteIncomingLetter(c *gin.Context) { - id, err := uuid.Parse(c.Param("id")) - if err != nil { - c.JSON(400, &contract.ErrorResponse{Error: "invalid id", Code: 400}) + id, ok := h.parseUUID(c, "id") + if !ok { return } + if err := h.svc.SoftDeleteIncomingLetter(c.Request.Context(), id); err != nil { - c.JSON(500, &contract.ErrorResponse{Error: err.Error(), Code: 500}) + h.handleServiceError(c, err) return } - c.JSON(200, &contract.SuccessResponse{Message: "deleted"}) + + h.respondSuccess(c, http.StatusOK, &contract.SuccessResponse{ + Message: "Letter deleted successfully", + }) } func (h *LetterHandler) CreateDispositions(c *gin.Context) { - appCtx := appcontext.FromGinContext(c.Request.Context()) var req contract.CreateLetterDispositionRequest - if err := c.ShouldBindJSON(&req); err != nil { - c.JSON(400, &contract.ErrorResponse{Error: "invalid body", Code: 400}) + if !h.bindJSON(c, &req) { return } + + // Extract department ID from context + appCtx := appcontext.FromGinContext(c.Request.Context()) req.FromDepartment = appCtx.DepartmentID + resp, err := h.svc.CreateDispositions(c.Request.Context(), &req) if err != nil { - c.JSON(500, &contract.ErrorResponse{Error: err.Error(), Code: 500}) + h.handleServiceError(c, err) return } - c.JSON(201, contract.BuildSuccessResponse(resp)) + + h.respondSuccess(c, http.StatusCreated, resp) } func (h *LetterHandler) GetEnhancedDispositionsByLetter(c *gin.Context) { - letterID, err := uuid.Parse(c.Param("letter_id")) - if err != nil { - c.JSON(400, &contract.ErrorResponse{Error: "invalid letter_id", Code: 400}) + letterID, ok := h.parseUUID(c, "letter_id") + if !ok { return } + resp, err := h.svc.GetEnhancedDispositionsByLetter(c.Request.Context(), letterID) if err != nil { - c.JSON(500, &contract.ErrorResponse{Error: err.Error(), Code: 500}) + h.handleServiceError(c, err) return } - c.JSON(200, contract.BuildSuccessResponse(resp)) + + h.respondSuccess(c, http.StatusOK, resp) } func (h *LetterHandler) CreateDiscussion(c *gin.Context) { - letterID, err := uuid.Parse(c.Param("letter_id")) - if err != nil { - c.JSON(400, &contract.ErrorResponse{Error: "invalid letter_id", Code: 400}) + letterID, ok := h.parseUUID(c, "letter_id") + if !ok { return } + var req contract.CreateLetterDiscussionRequest - if err := c.ShouldBindJSON(&req); err != nil { - c.JSON(400, &contract.ErrorResponse{Error: "invalid body", Code: 400}) + if !h.bindJSON(c, &req) { return } + resp, err := h.svc.CreateDiscussion(c.Request.Context(), letterID, &req) if err != nil { - c.JSON(500, &contract.ErrorResponse{Error: err.Error(), Code: 500}) + h.handleServiceError(c, err) return } - c.JSON(201, contract.BuildSuccessResponse(resp)) + + h.respondSuccess(c, http.StatusCreated, resp) } func (h *LetterHandler) UpdateDiscussion(c *gin.Context) { - letterID, err := uuid.Parse(c.Param("letter_id")) - if err != nil { - c.JSON(400, &contract.ErrorResponse{Error: "invalid letter_id", Code: 400}) + letterID, ok := h.parseUUID(c, "letter_id") + if !ok { return } - discussionID, err := uuid.Parse(c.Param("discussion_id")) - if err != nil { - c.JSON(400, &contract.ErrorResponse{Error: "invalid discussion_id", Code: 400}) + + discussionID, ok := h.parseUUID(c, "discussion_id") + if !ok { return } + var req contract.UpdateLetterDiscussionRequest - if err := c.ShouldBindJSON(&req); err != nil { - c.JSON(400, &contract.ErrorResponse{Error: "invalid body", Code: 400}) + if !h.bindJSON(c, &req) { return } + resp, err := h.svc.UpdateDiscussion(c.Request.Context(), letterID, discussionID, &req) if err != nil { - c.JSON(500, &contract.ErrorResponse{Error: err.Error(), Code: 500}) + h.handleServiceError(c, err) return } - c.JSON(200, contract.BuildSuccessResponse(resp)) + + h.respondSuccess(c, http.StatusOK, resp) } diff --git a/internal/handler/master_handler.go b/internal/handler/master_handler.go index 04b08b7..b23a420 100644 --- a/internal/handler/master_handler.go +++ b/internal/handler/master_handler.go @@ -30,6 +30,8 @@ type MasterService interface { UpdateDispositionAction(ctx context.Context, id uuid.UUID, req *contract.UpdateDispositionActionRequest) (*contract.DispositionActionResponse, error) DeleteDispositionAction(ctx context.Context, id uuid.UUID) error ListDispositionActions(ctx context.Context) (*contract.ListDispositionActionsResponse, error) + + ListDepartments(ctx context.Context, req *contract.ListDepartmentsRequest) (*contract.ListDepartmentsResponse, error) } type MasterHandler struct{ svc MasterService } @@ -256,3 +258,21 @@ func (h *MasterHandler) ListDispositionActions(c *gin.Context) { } c.JSON(200, contract.BuildSuccessResponse(resp)) } + +// Departments +func (h *MasterHandler) ListDepartments(c *gin.Context) { + var req contract.ListDepartmentsRequest + + // Parse query parameters + if err := c.ShouldBindQuery(&req); err != nil { + c.JSON(http.StatusBadRequest, &contract.ErrorResponse{Error: "invalid query parameters", Code: 400}) + return + } + + resp, err := h.svc.ListDepartments(c.Request.Context(), &req) + if err != nil { + c.JSON(http.StatusInternalServerError, &contract.ErrorResponse{Error: err.Error(), Code: 500}) + return + } + c.JSON(http.StatusOK, contract.BuildSuccessResponse(resp)) +} diff --git a/internal/processor/letter_number_generator.go b/internal/processor/letter_number_generator.go new file mode 100644 index 0000000..5e772c9 --- /dev/null +++ b/internal/processor/letter_number_generator.go @@ -0,0 +1,71 @@ +package processor + +import ( + "context" + "fmt" + + "eslogad-be/internal/entities" + "eslogad-be/internal/repository" +) + +// LetterNumberGenerator handles generation of letter numbers with configurable prefix and sequence +type LetterNumberGenerator interface { + GenerateNumber(ctx context.Context, prefixKey, sequenceKey string, defaultPrefix string) (string, error) +} + +type LetterNumberGeneratorImpl struct { + settingRepo *repository.AppSettingRepository +} + +func NewLetterNumberGenerator(settingRepo *repository.AppSettingRepository) *LetterNumberGeneratorImpl { + return &LetterNumberGeneratorImpl{ + settingRepo: settingRepo, + } +} + +func (g *LetterNumberGeneratorImpl) GenerateNumber(ctx context.Context, prefixKey, sequenceKey string, defaultPrefix string) (string, error) { + prefix := defaultPrefix + if s, err := g.settingRepo.Get(ctx, prefixKey); err == nil { + if v, ok := s.Value["value"].(string); ok && v != "" { + prefix = v + } + } + + seq := 0 + if s, err := g.settingRepo.Get(ctx, sequenceKey); err == nil { + if v, ok := s.Value["value"].(float64); ok { + seq = int(v) + } + } + + seq = seq + 1 + + letterNumber := fmt.Sprintf("%s%04d", prefix, seq) + + if err := g.settingRepo.Upsert(ctx, sequenceKey, entities.JSONB{"value": seq}); err != nil { + } + + return letterNumber, nil +} + +// GetCurrentSequence returns the current sequence number for a given key +func (g *LetterNumberGeneratorImpl) GetCurrentSequence(ctx context.Context, sequenceKey string) (int, error) { + seq := 0 + if s, err := g.settingRepo.Get(ctx, sequenceKey); err == nil { + if v, ok := s.Value["value"].(float64); ok { + seq = int(v) + } + } + return seq, nil +} + +// GetPrefix returns the configured prefix for a given key +func (g *LetterNumberGeneratorImpl) GetPrefix(ctx context.Context, prefixKey string, defaultPrefix string) string { + prefix := defaultPrefix + if s, err := g.settingRepo.Get(ctx, prefixKey); err == nil { + if v, ok := s.Value["value"].(string); ok && v != "" { + prefix = v + } + } + return prefix +} diff --git a/internal/processor/letter_outgoing_processor.go b/internal/processor/letter_outgoing_processor.go new file mode 100644 index 0000000..d5bdbb0 --- /dev/null +++ b/internal/processor/letter_outgoing_processor.go @@ -0,0 +1,539 @@ +package processor + +import ( + "context" + "time" + + "eslogad-be/internal/contract" + "eslogad-be/internal/entities" + "eslogad-be/internal/repository" + + "github.com/google/uuid" + "gorm.io/gorm" +) + +type LetterOutgoingProcessor interface { + CreateOutgoingLetter(ctx context.Context, letter *entities.LetterOutgoing, attachments []entities.LetterOutgoingAttachment, userID, departmentID uuid.UUID) error + GetOutgoingLetterByID(ctx context.Context, id uuid.UUID) (*entities.LetterOutgoing, error) + ListOutgoingLetters(ctx context.Context, filter repository.ListOutgoingLettersFilter, limit, offset int) ([]entities.LetterOutgoing, int64, error) + UpdateOutgoingLetter(ctx context.Context, letter *entities.LetterOutgoing, userID uuid.UUID) error + DeleteOutgoingLetter(ctx context.Context, id uuid.UUID, userID uuid.UUID) error + + UpdateLetterStatus(ctx context.Context, letterID uuid.UUID, status entities.LetterOutgoingStatus, userID uuid.UUID, fromStatus, toStatus *string) error + + ProcessApprovalSubmission(ctx context.Context, letterID uuid.UUID, approvalFlowID uuid.UUID, userID uuid.UUID) error + ProcessApproval(ctx context.Context, letterID uuid.UUID, approval *entities.LetterOutgoingApproval, userID uuid.UUID, allApproved bool) error + ProcessRejection(ctx context.Context, letterID uuid.UUID, approval *entities.LetterOutgoingApproval, userID uuid.UUID) error + + AddRecipients(ctx context.Context, letterID uuid.UUID, recipients []entities.LetterOutgoingRecipient, userID uuid.UUID) error + UpdateRecipient(ctx context.Context, recipient *entities.LetterOutgoingRecipient) error + RemoveRecipient(ctx context.Context, letterID uuid.UUID, recipientID uuid.UUID, userID uuid.UUID) error + + AddAttachments(ctx context.Context, letterID uuid.UUID, attachments []entities.LetterOutgoingAttachment, userID uuid.UUID) error + RemoveAttachment(ctx context.Context, letterID uuid.UUID, attachmentID uuid.UUID, userID uuid.UUID) error + + CreateDiscussion(ctx context.Context, discussion *entities.LetterOutgoingDiscussion, attachments []entities.LetterOutgoingDiscussionAttachment, userID uuid.UUID) error + GetDiscussionByID(ctx context.Context, id uuid.UUID) (*entities.LetterOutgoingDiscussion, error) + UpdateDiscussion(ctx context.Context, discussion *entities.LetterOutgoingDiscussion) error + DeleteDiscussion(ctx context.Context, id uuid.UUID) error + + GetApprovalsByLetter(ctx context.Context, letterID uuid.UUID) ([]entities.LetterOutgoingApproval, error) + GetApprovalFlow(ctx context.Context, flowID uuid.UUID) (*entities.ApprovalFlow, error) +} + +type LetterOutgoingProcessorImpl struct { + db *gorm.DB + letterRepo *repository.LetterOutgoingRepository + attachmentRepo *repository.LetterOutgoingAttachmentRepository + recipientRepo *repository.LetterOutgoingRecipientRepository + discussionRepo *repository.LetterOutgoingDiscussionRepository + discussionAttachmentRepo *repository.LetterOutgoingDiscussionAttachmentRepository + activityLogRepo *repository.LetterOutgoingActivityLogRepository + approvalFlowRepo *repository.ApprovalFlowRepository + approvalRepo *repository.LetterOutgoingApprovalRepository + numberGenerator *LetterNumberGeneratorImpl + txManager *repository.TxManager +} + +func NewLetterOutgoingProcessor( + db *gorm.DB, + letterRepo *repository.LetterOutgoingRepository, + attachmentRepo *repository.LetterOutgoingAttachmentRepository, + recipientRepo *repository.LetterOutgoingRecipientRepository, + discussionRepo *repository.LetterOutgoingDiscussionRepository, + discussionAttachmentRepo *repository.LetterOutgoingDiscussionAttachmentRepository, + activityLogRepo *repository.LetterOutgoingActivityLogRepository, + approvalFlowRepo *repository.ApprovalFlowRepository, + approvalRepo *repository.LetterOutgoingApprovalRepository, + numberGenerator *LetterNumberGeneratorImpl, + txManager *repository.TxManager, +) *LetterOutgoingProcessorImpl { + return &LetterOutgoingProcessorImpl{ + db: db, + letterRepo: letterRepo, + attachmentRepo: attachmentRepo, + recipientRepo: recipientRepo, + discussionRepo: discussionRepo, + discussionAttachmentRepo: discussionAttachmentRepo, + activityLogRepo: activityLogRepo, + approvalFlowRepo: approvalFlowRepo, + approvalRepo: approvalRepo, + numberGenerator: numberGenerator, + txManager: txManager, + } +} + +func (p *LetterOutgoingProcessorImpl) CreateOutgoingLetter(ctx context.Context, letter *entities.LetterOutgoing, attachments []entities.LetterOutgoingAttachment, userID, departmentID uuid.UUID) error { + return p.txManager.WithTransaction(ctx, func(txCtx context.Context) error { + // Step 1: Assign approval flow from department if not provided + if err := p.assignApprovalFlowFromDepartment(txCtx, letter, departmentID); err != nil { + // Log error but continue - approval flow is optional + } + + // Step 2: Set status based on approval flow + if letter.ApprovalFlowID != nil { + letter.Status = entities.LetterOutgoingStatusPendingApproval + } else { + letter.Status = entities.LetterOutgoingStatusApproved + } + + // Step 3: Generate and assign letter number + if err := p.assignLetterNumber(txCtx, letter); err != nil { + return err + } + + // Step 4: Create the letter + if err := p.letterRepo.Create(txCtx, letter); err != nil { + return err + } + + // Step 5: Create recipients from approval flow + if err := p.createRecipientsFromApprovalFlow(txCtx, letter); err != nil { + return err + } + + // Step 6: Create attachments + if err := p.createAttachments(txCtx, letter.ID, attachments); err != nil { + return err + } + + // Step 7: Log the activity + return p.logActivity(txCtx, letter.ID, entities.LetterOutgoingActionCreated, userID) + }) +} + +func (p *LetterOutgoingProcessorImpl) assignApprovalFlowFromDepartment(ctx context.Context, letter *entities.LetterOutgoing, departmentID uuid.UUID) error { + if letter.ApprovalFlowID != nil || departmentID == uuid.Nil { + return nil + } + + flow, err := p.approvalFlowRepo.GetByDepartment(ctx, departmentID) + if err != nil { + return err + } + + if flow != nil { + letter.ApprovalFlowID = &flow.ID + } + + return nil +} + +func (p *LetterOutgoingProcessorImpl) assignLetterNumber(ctx context.Context, letter *entities.LetterOutgoing) error { + letterNumber, err := p.numberGenerator.GenerateNumber( + ctx, + contract.SettingOutgoingLetterPrefix, + contract.SettingOutgoingLetterSequence, + "ESLO", + ) + + if err != nil { + return err + } + + letter.LetterNumber = letterNumber + + return nil +} + +func (p *LetterOutgoingProcessorImpl) createRecipientsFromApprovalFlow(ctx context.Context, letter *entities.LetterOutgoing) error { + if letter.ApprovalFlowID == nil { + return nil + } + + flow, err := p.approvalFlowRepo.Get(ctx, *letter.ApprovalFlowID) + if err != nil || flow == nil || len(flow.Steps) == 0 { + return err + } + + recipient := p.createRecipientFromApprovalStep(flow.Steps[0], letter.ID) + if recipient == nil { + return nil // No valid recipient could be created + } + + return p.recipientRepo.CreateBulk(ctx, []entities.LetterOutgoingRecipient{*recipient}) +} + +// createRecipientFromApprovalStep creates a recipient from an approval flow step +func (p *LetterOutgoingProcessorImpl) createRecipientFromApprovalStep(step entities.ApprovalFlowStep, letterID uuid.UUID) *entities.LetterOutgoingRecipient { + recipient := &entities.LetterOutgoingRecipient{ + LetterID: letterID, + IsPrimary: true, + } + + if step.ApproverUser != nil { + recipient.RecipientName = step.ApproverUser.Name + recipient.RecipientEmail = &step.ApproverUser.Email + + // Extract position from user profile if available + if step.ApproverUser.Profile != nil && step.ApproverUser.Profile.JobTitle != nil { + recipient.RecipientPosition = step.ApproverUser.Profile.JobTitle + } + } else if step.ApproverRole != nil { + recipient.RecipientName = step.ApproverRole.Name + position := "Role: " + step.ApproverRole.Name + recipient.RecipientPosition = &position + } else { + return nil // No valid approver found + } + + return recipient +} + +// createAttachments creates letter attachments +func (p *LetterOutgoingProcessorImpl) createAttachments(ctx context.Context, letterID uuid.UUID, attachments []entities.LetterOutgoingAttachment) error { + if len(attachments) == 0 { + return nil + } + + // Update letter IDs for all attachments + for i := range attachments { + attachments[i].LetterID = letterID + } + + return p.attachmentRepo.CreateBulk(ctx, attachments) +} + +// logActivity logs an activity for the letter +func (p *LetterOutgoingProcessorImpl) logActivity(ctx context.Context, letterID uuid.UUID, actionType string, userID uuid.UUID) error { + activityLog := &entities.LetterOutgoingActivityLog{ + LetterID: letterID, + ActionType: actionType, + ActorUserID: &userID, + } + return p.activityLogRepo.Create(ctx, activityLog) +} + +func (p *LetterOutgoingProcessorImpl) GetOutgoingLetterByID(ctx context.Context, id uuid.UUID) (*entities.LetterOutgoing, error) { + return p.letterRepo.Get(ctx, id) +} + +func (p *LetterOutgoingProcessorImpl) ListOutgoingLetters(ctx context.Context, filter repository.ListOutgoingLettersFilter, limit, offset int) ([]entities.LetterOutgoing, int64, error) { + return p.letterRepo.List(ctx, filter, limit, offset) +} + +func (p *LetterOutgoingProcessorImpl) UpdateOutgoingLetter(ctx context.Context, letter *entities.LetterOutgoing, userID uuid.UUID) error { + return p.txManager.WithTransaction(ctx, func(txCtx context.Context) error { + if err := p.letterRepo.Update(txCtx, letter); err != nil { + return err + } + + activityLog := &entities.LetterOutgoingActivityLog{ + LetterID: letter.ID, + ActionType: entities.LetterOutgoingActionUpdated, + ActorUserID: &userID, + } + if err := p.activityLogRepo.Create(txCtx, activityLog); err != nil { + return err + } + + return nil + }) +} + +func (p *LetterOutgoingProcessorImpl) DeleteOutgoingLetter(ctx context.Context, id uuid.UUID, userID uuid.UUID) error { + return p.txManager.WithTransaction(ctx, func(txCtx context.Context) error { + if err := p.letterRepo.SoftDelete(txCtx, id); err != nil { + return err + } + + letter, _ := p.letterRepo.Get(txCtx, id) + activityLog := &entities.LetterOutgoingActivityLog{ + LetterID: letter.ID, + ActionType: entities.LetterOutgoingActionDeleted, + ActorUserID: &userID, + } + if err := p.activityLogRepo.Create(txCtx, activityLog); err != nil { + return err + } + + return nil + }) +} + +func (p *LetterOutgoingProcessorImpl) UpdateLetterStatus(ctx context.Context, letterID uuid.UUID, status entities.LetterOutgoingStatus, userID uuid.UUID, fromStatus, toStatus *string) error { + return p.txManager.WithTransaction(ctx, func(txCtx context.Context) error { + if err := p.letterRepo.UpdateStatus(txCtx, letterID, status); err != nil { + return err + } + + activityLog := &entities.LetterOutgoingActivityLog{ + LetterID: letterID, + ActorUserID: &userID, + FromStatus: fromStatus, + ToStatus: toStatus, + } + + switch status { + case entities.LetterOutgoingStatusPendingApproval: + activityLog.ActionType = entities.LetterOutgoingActionSubmittedApproval + case entities.LetterOutgoingStatusApproved: + activityLog.ActionType = entities.LetterOutgoingActionApproved + case entities.LetterOutgoingStatusSent: + activityLog.ActionType = entities.LetterOutgoingActionSent + case entities.LetterOutgoingStatusArchived: + activityLog.ActionType = entities.LetterOutgoingActionArchived + default: + activityLog.ActionType = entities.LetterOutgoingActionUpdated + } + + if err := p.activityLogRepo.Create(txCtx, activityLog); err != nil { + return err + } + + return nil + }) +} + +func (p *LetterOutgoingProcessorImpl) ProcessApprovalSubmission(ctx context.Context, letterID uuid.UUID, approvalFlowID uuid.UUID, userID uuid.UUID) error { + flow, err := p.approvalFlowRepo.Get(ctx, approvalFlowID) + if err != nil { + return err + } + + return p.txManager.WithTransaction(ctx, func(txCtx context.Context) error { + approvals := make([]entities.LetterOutgoingApproval, len(flow.Steps)) + for i, step := range flow.Steps { + approvals[i] = entities.LetterOutgoingApproval{ + LetterID: letterID, + StepID: step.ID, + Status: entities.ApprovalStatusPending, + } + } + + if err := p.approvalRepo.CreateBulk(txCtx, approvals); err != nil { + return err + } + + if err := p.letterRepo.UpdateStatus(txCtx, letterID, entities.LetterOutgoingStatusPendingApproval); err != nil { + return err + } + + fromStatus := string(entities.LetterOutgoingStatusDraft) + toStatus := string(entities.LetterOutgoingStatusPendingApproval) + activityLog := &entities.LetterOutgoingActivityLog{ + LetterID: letterID, + ActionType: entities.LetterOutgoingActionSubmittedApproval, + ActorUserID: &userID, + FromStatus: &fromStatus, + ToStatus: &toStatus, + } + if err := p.activityLogRepo.Create(txCtx, activityLog); err != nil { + return err + } + + return nil + }) +} + +func (p *LetterOutgoingProcessorImpl) ProcessApproval(ctx context.Context, letterID uuid.UUID, approval *entities.LetterOutgoingApproval, userID uuid.UUID, allApproved bool) error { + return p.txManager.WithTransaction(ctx, func(txCtx context.Context) error { + now := time.Now() + approval.Status = entities.ApprovalStatusApproved + approval.ApproverID = &userID + approval.ActedAt = &now + + if err := p.approvalRepo.Update(txCtx, approval); err != nil { + return err + } + + if allApproved { + if err := p.letterRepo.UpdateStatus(txCtx, letterID, entities.LetterOutgoingStatusApproved); err != nil { + return err + } + } + + activityLog := &entities.LetterOutgoingActivityLog{ + LetterID: letterID, + ActionType: entities.LetterOutgoingActionApproved, + ActorUserID: &userID, + TargetID: &approval.ID, + } + if err := p.activityLogRepo.Create(txCtx, activityLog); err != nil { + return err + } + + return nil + }) +} + +func (p *LetterOutgoingProcessorImpl) ProcessRejection(ctx context.Context, letterID uuid.UUID, approval *entities.LetterOutgoingApproval, userID uuid.UUID) error { + return p.txManager.WithTransaction(ctx, func(txCtx context.Context) error { + now := time.Now() + approval.Status = entities.ApprovalStatusRejected + approval.ApproverID = &userID + approval.ActedAt = &now + + if err := p.approvalRepo.Update(txCtx, approval); err != nil { + return err + } + + if err := p.letterRepo.UpdateStatus(txCtx, letterID, entities.LetterOutgoingStatusDraft); err != nil { + return err + } + + fromStatus := string(entities.LetterOutgoingStatusPendingApproval) + toStatus := string(entities.LetterOutgoingStatusDraft) + activityLog := &entities.LetterOutgoingActivityLog{ + LetterID: letterID, + ActionType: entities.LetterOutgoingActionRejected, + ActorUserID: &userID, + TargetID: &approval.ID, + FromStatus: &fromStatus, + ToStatus: &toStatus, + } + if err := p.activityLogRepo.Create(txCtx, activityLog); err != nil { + return err + } + + return nil + }) +} + +func (p *LetterOutgoingProcessorImpl) AddRecipients(ctx context.Context, letterID uuid.UUID, recipients []entities.LetterOutgoingRecipient, userID uuid.UUID) error { + return p.txManager.WithTransaction(ctx, func(txCtx context.Context) error { + if err := p.recipientRepo.CreateBulk(txCtx, recipients); err != nil { + return err + } + + activityLog := &entities.LetterOutgoingActivityLog{ + LetterID: letterID, + ActionType: entities.LetterOutgoingActionRecipientAdded, + ActorUserID: &userID, + } + if err := p.activityLogRepo.Create(txCtx, activityLog); err != nil { + return err + } + + return nil + }) +} + +func (p *LetterOutgoingProcessorImpl) UpdateRecipient(ctx context.Context, recipient *entities.LetterOutgoingRecipient) error { + return p.recipientRepo.Update(ctx, recipient) +} + +func (p *LetterOutgoingProcessorImpl) RemoveRecipient(ctx context.Context, letterID uuid.UUID, recipientID uuid.UUID, userID uuid.UUID) error { + return p.txManager.WithTransaction(ctx, func(txCtx context.Context) error { + if err := p.recipientRepo.Delete(txCtx, recipientID); err != nil { + return err + } + + activityLog := &entities.LetterOutgoingActivityLog{ + LetterID: letterID, + ActionType: entities.LetterOutgoingActionRecipientRemoved, + ActorUserID: &userID, + TargetID: &recipientID, + } + if err := p.activityLogRepo.Create(txCtx, activityLog); err != nil { + return err + } + + return nil + }) +} + +func (p *LetterOutgoingProcessorImpl) AddAttachments(ctx context.Context, letterID uuid.UUID, attachments []entities.LetterOutgoingAttachment, userID uuid.UUID) error { + return p.txManager.WithTransaction(ctx, func(txCtx context.Context) error { + if err := p.attachmentRepo.CreateBulk(txCtx, attachments); err != nil { + return err + } + + activityLog := &entities.LetterOutgoingActivityLog{ + LetterID: letterID, + ActionType: entities.LetterOutgoingActionAttachmentAdded, + ActorUserID: &userID, + } + if err := p.activityLogRepo.Create(txCtx, activityLog); err != nil { + return err + } + + return nil + }) +} + +func (p *LetterOutgoingProcessorImpl) RemoveAttachment(ctx context.Context, letterID uuid.UUID, attachmentID uuid.UUID, userID uuid.UUID) error { + return p.txManager.WithTransaction(ctx, func(txCtx context.Context) error { + if err := p.attachmentRepo.Delete(txCtx, attachmentID); err != nil { + return err + } + + activityLog := &entities.LetterOutgoingActivityLog{ + LetterID: letterID, + ActionType: entities.LetterOutgoingActionAttachmentRemoved, + ActorUserID: &userID, + TargetID: &attachmentID, + } + if err := p.activityLogRepo.Create(txCtx, activityLog); err != nil { + return err + } + + return nil + }) +} + +func (p *LetterOutgoingProcessorImpl) CreateDiscussion(ctx context.Context, discussion *entities.LetterOutgoingDiscussion, attachments []entities.LetterOutgoingDiscussionAttachment, userID uuid.UUID) error { + return p.txManager.WithTransaction(ctx, func(txCtx context.Context) error { + if err := p.discussionRepo.Create(txCtx, discussion); err != nil { + return err + } + + if len(attachments) > 0 { + if err := p.discussionAttachmentRepo.CreateBulk(txCtx, attachments); err != nil { + return err + } + } + + activityLog := &entities.LetterOutgoingActivityLog{ + LetterID: discussion.LetterID, + ActionType: entities.LetterOutgoingActionDiscussionAdded, + ActorUserID: &userID, + TargetID: &discussion.ID, + } + if err := p.activityLogRepo.Create(txCtx, activityLog); err != nil { + return err + } + + return nil + }) +} + +func (p *LetterOutgoingProcessorImpl) GetDiscussionByID(ctx context.Context, id uuid.UUID) (*entities.LetterOutgoingDiscussion, error) { + return p.discussionRepo.Get(ctx, id) +} + +func (p *LetterOutgoingProcessorImpl) UpdateDiscussion(ctx context.Context, discussion *entities.LetterOutgoingDiscussion) error { + return p.discussionRepo.Update(ctx, discussion) +} + +func (p *LetterOutgoingProcessorImpl) DeleteDiscussion(ctx context.Context, id uuid.UUID) error { + return p.discussionRepo.Delete(ctx, id) +} + +func (p *LetterOutgoingProcessorImpl) GetApprovalsByLetter(ctx context.Context, letterID uuid.UUID) ([]entities.LetterOutgoingApproval, error) { + return p.approvalRepo.ListByLetter(ctx, letterID) +} + +func (p *LetterOutgoingProcessorImpl) GetApprovalFlow(ctx context.Context, flowID uuid.UUID) (*entities.ApprovalFlow, error) { + return p.approvalFlowRepo.Get(ctx, flowID) +} diff --git a/internal/processor/letter_processor.go b/internal/processor/letter_processor.go index bafdf3a..cc291d4 100644 --- a/internal/processor/letter_processor.go +++ b/internal/processor/letter_processor.go @@ -2,7 +2,6 @@ package processor import ( "context" - "fmt" "time" "eslogad-be/internal/appcontext" @@ -31,10 +30,11 @@ type LetterProcessorImpl struct { priorityRepo *repository.PriorityRepository institutionRepo *repository.InstitutionRepository dispActionRepo *repository.DispositionActionRepository + 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) *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} +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 (p *LetterProcessorImpl) CreateIncomingLetter(ctx context.Context, req *contract.CreateIncomingLetterRequest) (*contract.IncomingLetterResponse, error) { @@ -42,20 +42,15 @@ func (p *LetterProcessorImpl) CreateIncomingLetter(ctx context.Context, req *con err := p.txManager.WithTransaction(ctx, func(txCtx context.Context) error { userID := appcontext.FromGinContext(txCtx).UserID - prefix := "ESLI" - seq := 0 - if s, err := p.settingRepo.Get(txCtx, contract.SettingIncomingLetterPrefix); err == nil { - if v, ok := s.Value["value"].(string); ok && v != "" { - prefix = v - } + letterNumber, err := p.numberGenerator.GenerateNumber( + txCtx, + contract.SettingIncomingLetterPrefix, + contract.SettingIncomingLetterSequence, + "ESLI", + ) + if err != nil { + return err } - if s, err := p.settingRepo.Get(txCtx, contract.SettingIncomingLetterSequence); err == nil { - if v, ok := s.Value["value"].(float64); ok { - seq = int(v) - } - } - seq = seq + 1 - letterNumber := fmt.Sprintf("%s%04d", prefix, seq) entity := &entities.LetterIncoming{ ReferenceNumber: req.ReferenceNumber, @@ -73,8 +68,6 @@ func (p *LetterProcessorImpl) CreateIncomingLetter(ctx context.Context, req *con return err } - _ = p.settingRepo.Upsert(txCtx, contract.SettingIncomingLetterSequence, entities.JSONB{"value": seq}) - defaultDeptCodes := []string{} if s, err := p.settingRepo.Get(txCtx, contract.SettingIncomingLetterRecipients); err == nil { if arr, ok := s.Value["department_codes"].([]interface{}); ok { diff --git a/internal/processor/user_processor_test.go b/internal/processor/user_processor_test.go index bc9e9ca..674844e 100644 --- a/internal/processor/user_processor_test.go +++ b/internal/processor/user_processor_test.go @@ -4,7 +4,6 @@ import ( "context" "testing" - "eslogad-be/internal/contract" "eslogad-be/internal/entities" "github.com/google/uuid" diff --git a/internal/repository/app_setting_repository.go b/internal/repository/app_setting_repository.go new file mode 100644 index 0000000..db8e3ea --- /dev/null +++ b/internal/repository/app_setting_repository.go @@ -0,0 +1,28 @@ +package repository + +import ( + "context" + "eslogad-be/internal/entities" + "gorm.io/gorm" +) + +type AppSettingRepository struct{ db *gorm.DB } + +func NewAppSettingRepository(db *gorm.DB) *AppSettingRepository { + return &AppSettingRepository{ + db: db, + } +} + +func (r *AppSettingRepository) Get(ctx context.Context, key string) (*entities.AppSetting, error) { + db := DBFromContext(ctx, r.db) + var e entities.AppSetting + if err := db.WithContext(ctx).First(&e, "key = ?", key).Error; err != nil { + return nil, err + } + return &e, nil +} +func (r *AppSettingRepository) Upsert(ctx context.Context, key string, value entities.JSONB) error { + db := DBFromContext(ctx, r.db) + return db.WithContext(ctx).Exec("INSERT INTO app_settings(key, value) VALUES(?, ?) ON CONFLICT(key) DO UPDATE SET value = EXCLUDED.value, updated_at = CURRENT_TIMESTAMP", key, value).Error +} diff --git a/internal/repository/letter_repository.go b/internal/repository/letter_repository.go index 9225419..c237351 100644 --- a/internal/repository/letter_repository.go +++ b/internal/repository/letter_repository.go @@ -278,22 +278,6 @@ func (r *LetterDiscussionRepository) GetUsersByIDs(ctx context.Context, userIDs return users, nil } -type AppSettingRepository struct{ db *gorm.DB } - -func NewAppSettingRepository(db *gorm.DB) *AppSettingRepository { return &AppSettingRepository{db: db} } -func (r *AppSettingRepository) Get(ctx context.Context, key string) (*entities.AppSetting, error) { - db := DBFromContext(ctx, r.db) - var e entities.AppSetting - if err := db.WithContext(ctx).First(&e, "key = ?", key).Error; err != nil { - return nil, err - } - return &e, nil -} -func (r *AppSettingRepository) Upsert(ctx context.Context, key string, value entities.JSONB) error { - db := DBFromContext(ctx, r.db) - return db.WithContext(ctx).Exec("INSERT INTO app_settings(key, value) VALUES(?, ?) ON CONFLICT(key) DO UPDATE SET value = EXCLUDED.value, updated_at = CURRENT_TIMESTAMP", key, value).Error -} - // recipients type LetterIncomingRecipientRepository struct{ db *gorm.DB } diff --git a/internal/repository/master_repository.go b/internal/repository/master_repository.go index 83fae26..210060c 100644 --- a/internal/repository/master_repository.go +++ b/internal/repository/master_repository.go @@ -159,3 +159,32 @@ func (r *DepartmentRepository) Get(ctx context.Context, id uuid.UUID) (*entities } return &dep, nil } + +func (r *DepartmentRepository) List(ctx context.Context, search string, limit, offset int) ([]entities.Department, int64, error) { + db := DBFromContext(ctx, r.db) + + query := db.WithContext(ctx).Model(&entities.Department{}) + + // Add search filter if provided + if search != "" { + query = query.Where("name ILIKE ?", "%"+search+"%") + } + + // Get total count + var total int64 + if err := query.Count(&total).Error; err != nil { + return nil, 0, err + } + + // Get paginated results + var list []entities.Department + if err := query. + Order("name ASC"). + Limit(limit). + Offset(offset). + Find(&list).Error; err != nil { + return nil, 0, err + } + + return list, total, nil +} diff --git a/internal/router/health_handler.go b/internal/router/health_handler.go index 585a6b8..34a2da8 100644 --- a/internal/router/health_handler.go +++ b/internal/router/health_handler.go @@ -59,6 +59,8 @@ type MasterHandler interface { UpdateDispositionAction(c *gin.Context) DeleteDispositionAction(c *gin.Context) ListDispositionActions(c *gin.Context) + // departments + ListDepartments(c *gin.Context) } type LetterHandler interface { diff --git a/internal/router/router.go b/internal/router/router.go index a264a6d..9e14b3d 100644 --- a/internal/router/router.go +++ b/internal/router/router.go @@ -143,6 +143,8 @@ func (r *Router) addAppRoutes(rg *gin.Engine) { master.POST("/disposition-actions", r.masterHandler.CreateDispositionAction) master.PUT("/disposition-actions/:id", r.masterHandler.UpdateDispositionAction) master.DELETE("/disposition-actions/:id", r.masterHandler.DeleteDispositionAction) + + master.GET("/departments", r.masterHandler.ListDepartments) } lettersch := v1.Group("/letters") diff --git a/internal/service/letter_outgoing_service.go b/internal/service/letter_outgoing_service.go index 63223ce..90af0e3 100644 --- a/internal/service/letter_outgoing_service.go +++ b/internal/service/letter_outgoing_service.go @@ -2,10 +2,11 @@ package service import ( "context" - "time" + "eslogad-be/internal/appcontext" "eslogad-be/internal/contract" "eslogad-be/internal/entities" + "eslogad-be/internal/processor" "eslogad-be/internal/repository" "github.com/google/uuid" @@ -38,53 +39,24 @@ type LetterOutgoingService interface { } type LetterOutgoingServiceImpl struct { - db *gorm.DB - letterRepo *repository.LetterOutgoingRepository - attachmentRepo *repository.LetterOutgoingAttachmentRepository - recipientRepo *repository.LetterOutgoingRecipientRepository - discussionRepo *repository.LetterOutgoingDiscussionRepository - discussionAttachmentRepo *repository.LetterOutgoingDiscussionAttachmentRepository - activityLogRepo *repository.LetterOutgoingActivityLogRepository - approvalFlowRepo *repository.ApprovalFlowRepository - approvalRepo *repository.LetterOutgoingApprovalRepository - txManager *repository.TxManager + processor processor.LetterOutgoingProcessor } -func NewLetterOutgoingService( - db *gorm.DB, - letterRepo *repository.LetterOutgoingRepository, - attachmentRepo *repository.LetterOutgoingAttachmentRepository, - recipientRepo *repository.LetterOutgoingRecipientRepository, - discussionRepo *repository.LetterOutgoingDiscussionRepository, - discussionAttachmentRepo *repository.LetterOutgoingDiscussionAttachmentRepository, - activityLogRepo *repository.LetterOutgoingActivityLogRepository, - approvalFlowRepo *repository.ApprovalFlowRepository, - approvalRepo *repository.LetterOutgoingApprovalRepository, - txManager *repository.TxManager, -) *LetterOutgoingServiceImpl { +func NewLetterOutgoingService(processor processor.LetterOutgoingProcessor) *LetterOutgoingServiceImpl { return &LetterOutgoingServiceImpl{ - db: db, - letterRepo: letterRepo, - attachmentRepo: attachmentRepo, - recipientRepo: recipientRepo, - discussionRepo: discussionRepo, - discussionAttachmentRepo: discussionAttachmentRepo, - activityLogRepo: activityLogRepo, - approvalFlowRepo: approvalFlowRepo, - approvalRepo: approvalRepo, - txManager: txManager, + processor: processor, } } func (s *LetterOutgoingServiceImpl) CreateOutgoingLetter(ctx context.Context, req *contract.CreateOutgoingLetterRequest) (*contract.OutgoingLetterResponse, error) { + departmentID := getDepartmentIDFromContext(ctx) + letter := &entities.LetterOutgoing{ Subject: req.Subject, Description: req.Description, PriorityID: req.PriorityID, ReceiverInstitutionID: req.ReceiverInstitutionID, IssueDate: req.IssueDate, - Status: entities.LetterOutgoingStatusDraft, - ApprovalFlowID: req.ApprovalFlowID, CreatedBy: req.UserID, } @@ -92,61 +64,25 @@ func (s *LetterOutgoingServiceImpl) CreateOutgoingLetter(ctx context.Context, re letter.ReferenceNumber = req.ReferenceNumber } - err := s.txManager.WithTransaction(ctx, func(txCtx context.Context) error { - if err := s.letterRepo.Create(txCtx, letter); err != nil { - return err - } - - if len(req.Recipients) > 0 { - recipients := make([]entities.LetterOutgoingRecipient, len(req.Recipients)) - for i, r := range req.Recipients { - recipients[i] = entities.LetterOutgoingRecipient{ - LetterID: letter.ID, - RecipientName: r.Name, - RecipientEmail: r.Email, - RecipientPosition: r.Position, - RecipientInstitution: r.Institution, - IsPrimary: r.IsPrimary, - } - } - if err := s.recipientRepo.CreateBulk(txCtx, recipients); err != nil { - return err + var attachments []entities.LetterOutgoingAttachment + if len(req.Attachments) > 0 { + attachments = make([]entities.LetterOutgoingAttachment, len(req.Attachments)) + for i, a := range req.Attachments { + attachments[i] = entities.LetterOutgoingAttachment{ + FileURL: a.FileURL, + FileName: a.FileName, + FileType: a.FileType, + UploadedBy: &req.UserID, } } + } - if len(req.Attachments) > 0 { - attachments := make([]entities.LetterOutgoingAttachment, len(req.Attachments)) - for i, a := range req.Attachments { - attachments[i] = entities.LetterOutgoingAttachment{ - LetterID: letter.ID, - FileURL: a.FileURL, - FileName: a.FileName, - FileType: a.FileType, - UploadedBy: &req.UserID, - } - } - if err := s.attachmentRepo.CreateBulk(txCtx, attachments); err != nil { - return err - } - } - - activityLog := &entities.LetterOutgoingActivityLog{ - LetterID: letter.ID, - ActionType: entities.LetterOutgoingActionCreated, - ActorUserID: &req.UserID, - } - if err := s.activityLogRepo.Create(txCtx, activityLog); err != nil { - return err - } - - return nil - }) - + err := s.processor.CreateOutgoingLetter(ctx, letter, attachments, req.UserID, departmentID) if err != nil { return nil, err } - result, err := s.letterRepo.Get(ctx, letter.ID) + result, err := s.processor.GetOutgoingLetterByID(ctx, letter.ID) if err != nil { return nil, err } @@ -155,7 +91,7 @@ func (s *LetterOutgoingServiceImpl) CreateOutgoingLetter(ctx context.Context, re } func (s *LetterOutgoingServiceImpl) GetOutgoingLetterByID(ctx context.Context, id uuid.UUID) (*contract.OutgoingLetterResponse, error) { - letter, err := s.letterRepo.Get(ctx, id) + letter, err := s.processor.GetOutgoingLetterByID(ctx, id) if err != nil { return nil, err } @@ -173,7 +109,7 @@ func (s *LetterOutgoingServiceImpl) ListOutgoingLetters(ctx context.Context, req ToDate: req.ToDate, } - letters, total, err := s.letterRepo.List(ctx, filter, req.Limit, req.Offset) + letters, total, err := s.processor.ListOutgoingLetters(ctx, filter, req.Limit, req.Offset) if err != nil { return nil, err } @@ -192,7 +128,7 @@ func (s *LetterOutgoingServiceImpl) ListOutgoingLetters(ctx context.Context, req func (s *LetterOutgoingServiceImpl) UpdateOutgoingLetter(ctx context.Context, id uuid.UUID, req *contract.UpdateOutgoingLetterRequest) (*contract.OutgoingLetterResponse, error) { userID := getUserIDFromContext(ctx) - letter, err := s.letterRepo.Get(ctx, id) + letter, err := s.processor.GetOutgoingLetterByID(ctx, id) if err != nil { return nil, err } @@ -220,28 +156,12 @@ func (s *LetterOutgoingServiceImpl) UpdateOutgoingLetter(ctx context.Context, id letter.ReferenceNumber = req.ReferenceNumber } - err = s.txManager.WithTransaction(ctx, func(txCtx context.Context) error { - if err := s.letterRepo.Update(txCtx, letter); err != nil { - return err - } - - activityLog := &entities.LetterOutgoingActivityLog{ - LetterID: letter.ID, - ActionType: entities.LetterOutgoingActionUpdated, - ActorUserID: &userID, - } - if err := s.activityLogRepo.Create(txCtx, activityLog); err != nil { - return err - } - - return nil - }) - + err = s.processor.UpdateOutgoingLetter(ctx, letter, userID) if err != nil { return nil, err } - result, err := s.letterRepo.Get(ctx, id) + result, err := s.processor.GetOutgoingLetterByID(ctx, id) if err != nil { return nil, err } @@ -252,7 +172,7 @@ func (s *LetterOutgoingServiceImpl) UpdateOutgoingLetter(ctx context.Context, id func (s *LetterOutgoingServiceImpl) DeleteOutgoingLetter(ctx context.Context, id uuid.UUID) error { userID := getUserIDFromContext(ctx) - letter, err := s.letterRepo.Get(ctx, id) + letter, err := s.processor.GetOutgoingLetterByID(ctx, id) if err != nil { return err } @@ -261,28 +181,13 @@ func (s *LetterOutgoingServiceImpl) DeleteOutgoingLetter(ctx context.Context, id return gorm.ErrInvalidData } - return s.txManager.WithTransaction(ctx, func(txCtx context.Context) error { - if err := s.letterRepo.SoftDelete(txCtx, id); err != nil { - return err - } - - activityLog := &entities.LetterOutgoingActivityLog{ - LetterID: letter.ID, - ActionType: entities.LetterOutgoingActionDeleted, - ActorUserID: &userID, - } - if err := s.activityLogRepo.Create(txCtx, activityLog); err != nil { - return err - } - - return nil - }) + return s.processor.DeleteOutgoingLetter(ctx, id, userID) } func (s *LetterOutgoingServiceImpl) SubmitForApproval(ctx context.Context, letterID uuid.UUID) error { userID := getUserIDFromContext(ctx) - letter, err := s.letterRepo.Get(ctx, letterID) + letter, err := s.processor.GetOutgoingLetterByID(ctx, letterID) if err != nil { return err } @@ -295,48 +200,13 @@ func (s *LetterOutgoingServiceImpl) SubmitForApproval(ctx context.Context, lette return gorm.ErrInvalidData } - flow, err := s.approvalFlowRepo.Get(ctx, *letter.ApprovalFlowID) - if err != nil { - return err - } - - return s.txManager.WithTransaction(ctx, func(txCtx context.Context) error { - approvals := make([]entities.LetterOutgoingApproval, len(flow.Steps)) - for i, step := range flow.Steps { - approvals[i] = entities.LetterOutgoingApproval{ - LetterID: letterID, - StepID: step.ID, - Status: entities.ApprovalStatusPending, - } - } - - if err := s.approvalRepo.CreateBulk(txCtx, approvals); err != nil { - return err - } - - if err := s.letterRepo.UpdateStatus(txCtx, letterID, entities.LetterOutgoingStatusPendingApproval); err != nil { - return err - } - - activityLog := &entities.LetterOutgoingActivityLog{ - LetterID: letterID, - ActionType: entities.LetterOutgoingActionSubmittedApproval, - ActorUserID: &userID, - FromStatus: ptr(string(entities.LetterOutgoingStatusDraft)), - ToStatus: ptr(string(entities.LetterOutgoingStatusPendingApproval)), - } - if err := s.activityLogRepo.Create(txCtx, activityLog); err != nil { - return err - } - - return nil - }) + return s.processor.ProcessApprovalSubmission(ctx, letterID, *letter.ApprovalFlowID, userID) } func (s *LetterOutgoingServiceImpl) ApproveOutgoingLetter(ctx context.Context, letterID uuid.UUID, req *contract.ApproveLetterRequest) error { userID := getUserIDFromContext(ctx) - letter, err := s.letterRepo.Get(ctx, letterID) + letter, err := s.processor.GetOutgoingLetterByID(ctx, letterID) if err != nil { return err } @@ -345,7 +215,7 @@ func (s *LetterOutgoingServiceImpl) ApproveOutgoingLetter(ctx context.Context, l return gorm.ErrInvalidData } - approvals, err := s.approvalRepo.ListByLetter(ctx, letterID) + approvals, err := s.processor.GetApprovalsByLetter(ctx, letterID) if err != nil { return err } @@ -366,49 +236,23 @@ func (s *LetterOutgoingServiceImpl) ApproveOutgoingLetter(ctx context.Context, l return gorm.ErrInvalidData } - return s.txManager.WithTransaction(ctx, func(txCtx context.Context) error { - now := time.Now() - currentApproval.Status = entities.ApprovalStatusApproved - currentApproval.ApproverID = &userID - currentApproval.ActedAt = &now - currentApproval.Remarks = req.Remarks + currentApproval.Remarks = req.Remarks - if err := s.approvalRepo.Update(txCtx, currentApproval); err != nil { - return err + allApproved := true + for _, approval := range approvals { + if approval.ID != currentApproval.ID && approval.Status == entities.ApprovalStatusPending { + allApproved = false + break } + } - allApproved := true - for _, approval := range approvals { - if approval.ID != currentApproval.ID && approval.Status == entities.ApprovalStatusPending { - allApproved = false - break - } - } - - if allApproved { - if err := s.letterRepo.UpdateStatus(txCtx, letterID, entities.LetterOutgoingStatusApproved); err != nil { - return err - } - } - - activityLog := &entities.LetterOutgoingActivityLog{ - LetterID: letterID, - ActionType: entities.LetterOutgoingActionApproved, - ActorUserID: &userID, - TargetID: ¤tApproval.ID, - } - if err := s.activityLogRepo.Create(txCtx, activityLog); err != nil { - return err - } - - return nil - }) + return s.processor.ProcessApproval(ctx, letterID, currentApproval, userID, allApproved) } func (s *LetterOutgoingServiceImpl) RejectOutgoingLetter(ctx context.Context, letterID uuid.UUID, req *contract.RejectLetterRequest) error { userID := getUserIDFromContext(ctx) - letter, err := s.letterRepo.Get(ctx, letterID) + letter, err := s.processor.GetOutgoingLetterByID(ctx, letterID) if err != nil { return err } @@ -417,7 +261,7 @@ func (s *LetterOutgoingServiceImpl) RejectOutgoingLetter(ctx context.Context, le return gorm.ErrInvalidData } - approvals, err := s.approvalRepo.ListByLetter(ctx, letterID) + approvals, err := s.processor.GetApprovalsByLetter(ctx, letterID) if err != nil { return err } @@ -438,41 +282,15 @@ func (s *LetterOutgoingServiceImpl) RejectOutgoingLetter(ctx context.Context, le return gorm.ErrInvalidData } - return s.txManager.WithTransaction(ctx, func(txCtx context.Context) error { - now := time.Now() - currentApproval.Status = entities.ApprovalStatusRejected - currentApproval.ApproverID = &userID - currentApproval.ActedAt = &now - currentApproval.Remarks = &req.Reason + currentApproval.Remarks = &req.Reason - if err := s.approvalRepo.Update(txCtx, currentApproval); err != nil { - return err - } - - if err := s.letterRepo.UpdateStatus(txCtx, letterID, entities.LetterOutgoingStatusDraft); err != nil { - return err - } - - activityLog := &entities.LetterOutgoingActivityLog{ - LetterID: letterID, - ActionType: entities.LetterOutgoingActionRejected, - ActorUserID: &userID, - TargetID: ¤tApproval.ID, - FromStatus: ptr(string(entities.LetterOutgoingStatusPendingApproval)), - ToStatus: ptr(string(entities.LetterOutgoingStatusDraft)), - } - if err := s.activityLogRepo.Create(txCtx, activityLog); err != nil { - return err - } - - return nil - }) + return s.processor.ProcessRejection(ctx, letterID, currentApproval, userID) } func (s *LetterOutgoingServiceImpl) SendOutgoingLetter(ctx context.Context, letterID uuid.UUID) error { userID := getUserIDFromContext(ctx) - letter, err := s.letterRepo.Get(ctx, letterID) + letter, err := s.processor.GetOutgoingLetterByID(ctx, letterID) if err != nil { return err } @@ -481,30 +299,15 @@ func (s *LetterOutgoingServiceImpl) SendOutgoingLetter(ctx context.Context, lett return gorm.ErrInvalidData } - return s.txManager.WithTransaction(ctx, func(txCtx context.Context) error { - if err := s.letterRepo.UpdateStatus(txCtx, letterID, entities.LetterOutgoingStatusSent); err != nil { - return err - } - - activityLog := &entities.LetterOutgoingActivityLog{ - LetterID: letterID, - ActionType: entities.LetterOutgoingActionSent, - ActorUserID: &userID, - FromStatus: ptr(string(entities.LetterOutgoingStatusApproved)), - ToStatus: ptr(string(entities.LetterOutgoingStatusSent)), - } - if err := s.activityLogRepo.Create(txCtx, activityLog); err != nil { - return err - } - - return nil - }) + fromStatus := string(entities.LetterOutgoingStatusApproved) + toStatus := string(entities.LetterOutgoingStatusSent) + return s.processor.UpdateLetterStatus(ctx, letterID, entities.LetterOutgoingStatusSent, userID, &fromStatus, &toStatus) } func (s *LetterOutgoingServiceImpl) ArchiveOutgoingLetter(ctx context.Context, letterID uuid.UUID) error { userID := getUserIDFromContext(ctx) - letter, err := s.letterRepo.Get(ctx, letterID) + letter, err := s.processor.GetOutgoingLetterByID(ctx, letterID) if err != nil { return err } @@ -513,30 +316,15 @@ func (s *LetterOutgoingServiceImpl) ArchiveOutgoingLetter(ctx context.Context, l return gorm.ErrInvalidData } - return s.txManager.WithTransaction(ctx, func(txCtx context.Context) error { - if err := s.letterRepo.UpdateStatus(txCtx, letterID, entities.LetterOutgoingStatusArchived); err != nil { - return err - } - - activityLog := &entities.LetterOutgoingActivityLog{ - LetterID: letterID, - ActionType: entities.LetterOutgoingActionArchived, - ActorUserID: &userID, - FromStatus: ptr(string(entities.LetterOutgoingStatusSent)), - ToStatus: ptr(string(entities.LetterOutgoingStatusArchived)), - } - if err := s.activityLogRepo.Create(txCtx, activityLog); err != nil { - return err - } - - return nil - }) + fromStatus := string(entities.LetterOutgoingStatusSent) + toStatus := string(entities.LetterOutgoingStatusArchived) + return s.processor.UpdateLetterStatus(ctx, letterID, entities.LetterOutgoingStatusArchived, userID, &fromStatus, &toStatus) } func (s *LetterOutgoingServiceImpl) AddRecipients(ctx context.Context, letterID uuid.UUID, req *contract.AddRecipientsRequest) error { userID := getUserIDFromContext(ctx) - letter, err := s.letterRepo.Get(ctx, letterID) + letter, err := s.processor.GetOutgoingLetterByID(ctx, letterID) if err != nil { return err } @@ -557,26 +345,11 @@ func (s *LetterOutgoingServiceImpl) AddRecipients(ctx context.Context, letterID } } - return s.txManager.WithTransaction(ctx, func(txCtx context.Context) error { - if err := s.recipientRepo.CreateBulk(txCtx, recipients); err != nil { - return err - } - - activityLog := &entities.LetterOutgoingActivityLog{ - LetterID: letterID, - ActionType: entities.LetterOutgoingActionRecipientAdded, - ActorUserID: &userID, - } - if err := s.activityLogRepo.Create(txCtx, activityLog); err != nil { - return err - } - - return nil - }) + return s.processor.AddRecipients(ctx, letterID, recipients, userID) } func (s *LetterOutgoingServiceImpl) UpdateRecipient(ctx context.Context, letterID uuid.UUID, recipientID uuid.UUID, req *contract.UpdateRecipientRequest) error { - letter, err := s.letterRepo.Get(ctx, letterID) + letter, err := s.processor.GetOutgoingLetterByID(ctx, letterID) if err != nil { return err } @@ -594,13 +367,13 @@ func (s *LetterOutgoingServiceImpl) UpdateRecipient(ctx context.Context, letterI IsPrimary: req.IsPrimary, } - return s.recipientRepo.Update(ctx, recipient) + return s.processor.UpdateRecipient(ctx, recipient) } func (s *LetterOutgoingServiceImpl) RemoveRecipient(ctx context.Context, letterID uuid.UUID, recipientID uuid.UUID) error { userID := getUserIDFromContext(ctx) - letter, err := s.letterRepo.Get(ctx, letterID) + letter, err := s.processor.GetOutgoingLetterByID(ctx, letterID) if err != nil { return err } @@ -609,29 +382,13 @@ func (s *LetterOutgoingServiceImpl) RemoveRecipient(ctx context.Context, letterI return gorm.ErrInvalidData } - return s.txManager.WithTransaction(ctx, func(txCtx context.Context) error { - if err := s.recipientRepo.Delete(txCtx, recipientID); err != nil { - return err - } - - activityLog := &entities.LetterOutgoingActivityLog{ - LetterID: letterID, - ActionType: entities.LetterOutgoingActionRecipientRemoved, - ActorUserID: &userID, - TargetID: &recipientID, - } - if err := s.activityLogRepo.Create(txCtx, activityLog); err != nil { - return err - } - - return nil - }) + return s.processor.RemoveRecipient(ctx, letterID, recipientID, userID) } func (s *LetterOutgoingServiceImpl) AddAttachments(ctx context.Context, letterID uuid.UUID, req *contract.AddAttachmentsRequest) error { userID := getUserIDFromContext(ctx) - letter, err := s.letterRepo.Get(ctx, letterID) + letter, err := s.processor.GetOutgoingLetterByID(ctx, letterID) if err != nil { return err } @@ -651,28 +408,13 @@ func (s *LetterOutgoingServiceImpl) AddAttachments(ctx context.Context, letterID } } - return s.txManager.WithTransaction(ctx, func(txCtx context.Context) error { - if err := s.attachmentRepo.CreateBulk(txCtx, attachments); err != nil { - return err - } - - activityLog := &entities.LetterOutgoingActivityLog{ - LetterID: letterID, - ActionType: entities.LetterOutgoingActionAttachmentAdded, - ActorUserID: &userID, - } - if err := s.activityLogRepo.Create(txCtx, activityLog); err != nil { - return err - } - - return nil - }) + return s.processor.AddAttachments(ctx, letterID, attachments, userID) } func (s *LetterOutgoingServiceImpl) RemoveAttachment(ctx context.Context, letterID uuid.UUID, attachmentID uuid.UUID) error { userID := getUserIDFromContext(ctx) - letter, err := s.letterRepo.Get(ctx, letterID) + letter, err := s.processor.GetOutgoingLetterByID(ctx, letterID) if err != nil { return err } @@ -681,23 +423,7 @@ func (s *LetterOutgoingServiceImpl) RemoveAttachment(ctx context.Context, letter return gorm.ErrInvalidData } - return s.txManager.WithTransaction(ctx, func(txCtx context.Context) error { - if err := s.attachmentRepo.Delete(txCtx, attachmentID); err != nil { - return err - } - - activityLog := &entities.LetterOutgoingActivityLog{ - LetterID: letterID, - ActionType: entities.LetterOutgoingActionAttachmentRemoved, - ActorUserID: &userID, - TargetID: &attachmentID, - } - if err := s.activityLogRepo.Create(txCtx, activityLog); err != nil { - return err - } - - return nil - }) + return s.processor.RemoveAttachment(ctx, letterID, attachmentID, userID) } func (s *LetterOutgoingServiceImpl) CreateDiscussion(ctx context.Context, letterID uuid.UUID, req *contract.CreateDiscussionRequest) (*contract.DiscussionResponse, error) { @@ -714,45 +440,27 @@ func (s *LetterOutgoingServiceImpl) CreateDiscussion(ctx context.Context, letter discussion.Mentions = req.Mentions } - err := s.txManager.WithTransaction(ctx, func(txCtx context.Context) error { - if err := s.discussionRepo.Create(txCtx, discussion); err != nil { - return err - } - - if len(req.Attachments) > 0 { - attachments := make([]entities.LetterOutgoingDiscussionAttachment, len(req.Attachments)) - for i, a := range req.Attachments { - attachments[i] = entities.LetterOutgoingDiscussionAttachment{ - DiscussionID: discussion.ID, - FileURL: a.FileURL, - FileName: a.FileName, - FileType: a.FileType, - UploadedBy: &userID, - } - } - if err := s.discussionAttachmentRepo.CreateBulk(txCtx, attachments); err != nil { - return err + var attachments []entities.LetterOutgoingDiscussionAttachment + if len(req.Attachments) > 0 { + attachments = make([]entities.LetterOutgoingDiscussionAttachment, len(req.Attachments)) + for i, a := range req.Attachments { + attachments[i] = entities.LetterOutgoingDiscussionAttachment{ + DiscussionID: discussion.ID, + FileURL: a.FileURL, + FileName: a.FileName, + FileType: a.FileType, + UploadedBy: &userID, } } + } - activityLog := &entities.LetterOutgoingActivityLog{ - LetterID: letterID, - ActionType: entities.LetterOutgoingActionDiscussionAdded, - ActorUserID: &userID, - TargetID: &discussion.ID, - } - if err := s.activityLogRepo.Create(txCtx, activityLog); err != nil { - return err - } - - return nil - }) + err := s.processor.CreateDiscussion(ctx, discussion, attachments, userID) if err != nil { return nil, err } - result, err := s.discussionRepo.Get(ctx, discussion.ID) + result, err := s.processor.GetDiscussionByID(ctx, discussion.ID) if err != nil { return nil, err } @@ -761,7 +469,7 @@ func (s *LetterOutgoingServiceImpl) CreateDiscussion(ctx context.Context, letter } func (s *LetterOutgoingServiceImpl) UpdateDiscussion(ctx context.Context, discussionID uuid.UUID, req *contract.UpdateDiscussionRequest) error { - discussion, err := s.discussionRepo.Get(ctx, discussionID) + discussion, err := s.processor.GetDiscussionByID(ctx, discussionID) if err != nil { return err } @@ -776,11 +484,11 @@ func (s *LetterOutgoingServiceImpl) UpdateDiscussion(ctx context.Context, discus discussion.Mentions = req.Mentions } - return s.discussionRepo.Update(ctx, discussion) + return s.processor.UpdateDiscussion(ctx, discussion) } func (s *LetterOutgoingServiceImpl) DeleteDiscussion(ctx context.Context, discussionID uuid.UUID) error { - discussion, err := s.discussionRepo.Get(ctx, discussionID) + discussion, err := s.processor.GetDiscussionByID(ctx, discussionID) if err != nil { return err } @@ -790,21 +498,29 @@ func (s *LetterOutgoingServiceImpl) DeleteDiscussion(ctx context.Context, discus return gorm.ErrInvalidData } - return s.discussionRepo.Delete(ctx, discussionID) + return s.processor.DeleteDiscussion(ctx, discussionID) } func getUserIDFromContext(ctx context.Context) uuid.UUID { + appCtx := appcontext.FromGinContext(ctx) + if appCtx != nil { + return appCtx.UserID + } return uuid.New() } +func getDepartmentIDFromContext(ctx context.Context) uuid.UUID { + appCtx := appcontext.FromGinContext(ctx) + if appCtx != nil { + return appCtx.DepartmentID + } + return uuid.Nil +} + func userHasRole(ctx context.Context, roleID uuid.UUID) bool { return false } -func ptr(s string) *string { - return &s -} - func transformLetterToResponse(letter *entities.LetterOutgoing) *contract.OutgoingLetterResponse { resp := &contract.OutgoingLetterResponse{ ID: letter.ID, diff --git a/internal/service/master_service.go b/internal/service/master_service.go index f52e369..68b45a8 100644 --- a/internal/service/master_service.go +++ b/internal/service/master_service.go @@ -16,10 +16,11 @@ type MasterServiceImpl struct { priorityRepo *repository.PriorityRepository institutionRepo *repository.InstitutionRepository dispRepo *repository.DispositionActionRepository + departmentRepo *repository.DepartmentRepository } -func NewMasterService(label *repository.LabelRepository, priority *repository.PriorityRepository, institution *repository.InstitutionRepository, disp *repository.DispositionActionRepository) *MasterServiceImpl { - return &MasterServiceImpl{labelRepo: label, priorityRepo: priority, institutionRepo: institution, dispRepo: disp} +func NewMasterService(label *repository.LabelRepository, priority *repository.PriorityRepository, institution *repository.InstitutionRepository, disp *repository.DispositionActionRepository, department *repository.DepartmentRepository) *MasterServiceImpl { + return &MasterServiceImpl{labelRepo: label, priorityRepo: priority, institutionRepo: institution, dispRepo: disp, departmentRepo: department} } // Labels @@ -212,3 +213,34 @@ func (s *MasterServiceImpl) ListDispositionActions(ctx context.Context) (*contra } return &contract.ListDispositionActionsResponse{Actions: transformer.DispositionActionsToContract(list)}, nil } + +// Departments +func (s *MasterServiceImpl) ListDepartments(ctx context.Context, req *contract.ListDepartmentsRequest) (*contract.ListDepartmentsResponse, error) { + // Set default values if not provided + page := req.Page + if page < 1 { + page = 1 + } + + limit := req.Limit + if limit < 1 { + limit = 10 + } + if limit > 100 { + limit = 100 // Max limit to prevent performance issues + } + + offset := (page - 1) * limit + + list, total, err := s.departmentRepo.List(ctx, req.Search, limit, offset) + if err != nil { + return nil, err + } + + return &contract.ListDepartmentsResponse{ + Departments: transformer.DepartmentsToContract(list), + Total: total, + Page: page, + Limit: limit, + }, nil +} diff --git a/internal/transformer/common_transformer.go b/internal/transformer/common_transformer.go index 919932f..45ee4f7 100644 --- a/internal/transformer/common_transformer.go +++ b/internal/transformer/common_transformer.go @@ -84,13 +84,23 @@ func DepartmentsToContract(positions []entities.Department) []contract.Departmen } res := make([]contract.DepartmentResponse, 0, len(positions)) for _, p := range positions { - res = append(res, contract.DepartmentResponse{ID: p.ID, Name: p.Name, Code: p.Code, Path: p.Path}) + res = append(res, contract.DepartmentResponse{ + ID: p.ID, + Name: p.Name, + Code: p.Code, + Path: p.Path, + }) } return res } func DepartmentToContract(p entities.Department) contract.DepartmentResponse { - return contract.DepartmentResponse{ID: p.ID, Name: p.Name, Code: p.Code, Path: p.Path} + return contract.DepartmentResponse{ + ID: p.ID, + Name: p.Name, + Code: p.Code, + Path: p.Path, + } } func ProfileEntityToContract(p *entities.UserProfile) *contract.UserProfileResponse {