diff --git a/internal/app/app.go b/internal/app/app.go index 9da475f..6b526e3 100644 --- a/internal/app/app.go +++ b/internal/app/app.go @@ -53,6 +53,7 @@ func (a *App) Initialize(cfg *config.Config) error { onlyOfficeHandler := handler.NewOnlyOfficeHandler(services.onlyOfficeService) analyticsHandler := handler.NewAnalyticsHandler(services.analyticsService) notificationHandler := handler.NewNotificationHandler(services.notificationService) + repositoryAttachmentHandler := handler.NewRepositoryAttachmentHandler(services.repositoryAttachmentService) a.router = router.NewRouter( cfg, @@ -70,6 +71,7 @@ func (a *App) Initialize(cfg *config.Config) error { onlyOfficeHandler, analyticsHandler, notificationHandler, + repositoryAttachmentHandler, ) return nil @@ -147,6 +149,7 @@ type repositories struct { approvalFlowRepo *repository.ApprovalFlowRepository letterOutgoingApprovalRepo *repository.LetterOutgoingApprovalRepository analyticsRepo *repository.AnalyticsRepository + repositoryAttachmentRepo *repository.RepositoryAttachmentRepositoryImpl } func (a *App) initRepositories() *repositories { @@ -181,6 +184,7 @@ func (a *App) initRepositories() *repositories { approvalFlowRepo: repository.NewApprovalFlowRepository(a.db), letterOutgoingApprovalRepo: repository.NewLetterOutgoingApprovalRepository(a.db), analyticsRepo: repository.NewAnalyticsRepository(a.db), + repositoryAttachmentRepo: repository.NewRepositoryAttachmentRepositoryImpl(a.db), } } @@ -198,13 +202,14 @@ type processors struct { letterDispositionProcessor *processor.LetterDispositionProcessorImpl letterDispositionDeptProcessor *processor.LetterDispositionDepartmentProcessorImpl // Modular processors for letter outgoing - letterValidationProcessor processor.LetterValidationProcessor - letterCreationProcessor processor.LetterCreationProcessor - letterApprovalProcessor processor.LetterApprovalProcessor - letterAttachmentProcessor processor.LetterAttachmentProcessor - letterOutgoingRecipientProcessor processor.LetterOutgoingRecipientProcessor - letterActivityProcessor processor.LetterActivityProcessor - txManager *repository.TxManager + letterValidationProcessor processor.LetterValidationProcessor + letterCreationProcessor processor.LetterCreationProcessor + letterApprovalProcessor processor.LetterApprovalProcessor + letterAttachmentProcessor processor.LetterAttachmentProcessor + letterOutgoingRecipientProcessor processor.LetterOutgoingRecipientProcessor + letterActivityProcessor processor.LetterActivityProcessor + repositoryAttachmentProcessor *processor.RepositoryAttachmentProcessorImpl + txManager *repository.TxManager } func (a *App) initProcessors(cfg *config.Config, repos *repositories) *processors { @@ -249,7 +254,7 @@ func (a *App) initProcessors(cfg *config.Config, repos *repositories) *processor letterActivityProc := processor.NewLetterActivityProcessor( repos.letterOutgoingActivityLogRepo, ) - + // Create the main letter outgoing processor for backward compatibility letterOutgoingProc := processor.NewLetterOutgoingProcessor( a.db, @@ -286,14 +291,14 @@ func (a *App) initProcessors(cfg *config.Config, repos *repositories) *processor // Create Novu processor for backward compatibility novuConfig := internalConfig.LoadNovuConfig(cfg) novuProc := processor.NewNovuProcessor(novuConfig) - + // Create notification processor with Novu provider novuProvider := processor.NewNovuProvider(novuConfig) notificationProc := processor.NewNotificationProcessor(novuProvider, novuConfig.IncomingLetterWorkflowID) // Create user role processor userRoleProc := processor.NewUserRoleProcessor(a.db) - + // Create user processor with Novu integration userProc := processor.NewUserProcessor(repos.userRepo, repos.userProfileRepo, userRoleProc) userProc.SetNovuProcessor(novuProc) @@ -327,6 +332,9 @@ func (a *App) initProcessors(cfg *config.Config, repos *repositories) *processor repos.letterRepo, ) + repositoryAttachmentProc := processor.NewRepositoryAttachmentProcessor( + repos.repositoryAttachmentRepo) + return &processors{ userProcessor: userProc, cachedUserProcessor: cachedUserProc, @@ -347,23 +355,25 @@ func (a *App) initProcessors(cfg *config.Config, repos *repositories) *processor letterAttachmentProcessor: letterAttachmentProc, letterOutgoingRecipientProcessor: letterOutgoingRecipientProc, letterActivityProcessor: letterActivityProc, + repositoryAttachmentProcessor: repositoryAttachmentProc, txManager: txMgr, } } type services struct { - userService *service.UserServiceImpl - authService *service.AuthServiceImpl - fileService *service.FileServiceImpl - rbacService *service.RBACServiceImpl - masterService *service.MasterServiceImpl - letterService *service.LetterServiceImpl - letterOutgoingService *service.LetterOutgoingServiceImpl - approvalFlowService *service.ApprovalFlowServiceImpl - dispositionRouteService *service.DispositionRouteServiceImpl - onlyOfficeService *service.OnlyOfficeServiceImpl - analyticsService *service.AnalyticsServiceImpl - notificationService *service.NotificationServiceImpl + userService *service.UserServiceImpl + authService *service.AuthServiceImpl + fileService *service.FileServiceImpl + rbacService *service.RBACServiceImpl + masterService *service.MasterServiceImpl + letterService *service.LetterServiceImpl + letterOutgoingService *service.LetterOutgoingServiceImpl + approvalFlowService *service.ApprovalFlowServiceImpl + dispositionRouteService *service.DispositionRouteServiceImpl + onlyOfficeService *service.OnlyOfficeServiceImpl + analyticsService *service.AnalyticsServiceImpl + notificationService *service.NotificationServiceImpl + repositoryAttachmentService *service.RepositoryAttachmentServiceImpl } func (a *App) initServices(processors *processors, repos *repositories, cfg *config.Config) *services { @@ -423,19 +433,22 @@ func (a *App) initServices(processors *processors, repos *repositories, cfg *con novuConfig := internalConfig.LoadNovuConfig(cfg) notificationSvc := service.NewNotificationService(novuConfig, processors.userProcessor) + repositoryAttachmentSvc := service.NewRepositoryAttachmentService(processors.repositoryAttachmentProcessor) + return &services{ - userService: userSvc, - authService: authService, - fileService: fileSvc, - rbacService: rbacSvc, - masterService: masterSvc, - letterService: letterSvc, - letterOutgoingService: letterOutgoingSvc, - approvalFlowService: approvalFlowSvc, - dispositionRouteService: dispRouteSvc, - onlyOfficeService: onlyOfficeSvc, - analyticsService: analyticsSvc, - notificationService: notificationSvc, + userService: userSvc, + authService: authService, + fileService: fileSvc, + rbacService: rbacSvc, + masterService: masterSvc, + letterService: letterSvc, + letterOutgoingService: letterOutgoingSvc, + approvalFlowService: approvalFlowSvc, + dispositionRouteService: dispRouteSvc, + onlyOfficeService: onlyOfficeSvc, + analyticsService: analyticsSvc, + notificationService: notificationSvc, + repositoryAttachmentService: repositoryAttachmentSvc, } } diff --git a/internal/contract/repository_attachment_contract.go b/internal/contract/repository_attachment_contract.go new file mode 100644 index 0000000..df88613 --- /dev/null +++ b/internal/contract/repository_attachment_contract.go @@ -0,0 +1,35 @@ +package contract + +import ( + "time" + + "github.com/google/uuid" +) + +type CreateRepositoryAttachmentRequest struct { + FileURL string `json:"file_url" validate:"required"` + FileName string `json:"file_name" validate:"required"` + FileType string `json:"file_type" validate:"required"` + Category string `json:"category" validate:"required"` +} + +type RepositoryAttachmentsResponse struct { + ID uuid.UUID `json:"id"` + FileURL string `json:"file_url"` + FileName string `json:"file_name"` + FileType string `json:"file_type"` + Category string `json:"category"` + UploadBy uuid.UUID `json:"upload_by"` + UploadAt time.Time `json:"upload_at"` +} + +type ListRepositoryAttachmentsResponse struct { + Attachments []RepositoryAttachmentsResponse `json:"attachments"` + Pagination PaginationResponse `json:"pagination"` +} + +type ListRepositoryAttachmentsRequest struct { + Page int `json:"page"` + Limit int `json:"limit"` + Search *string `json:"search,omitempty"` +} diff --git a/internal/entities/repository_attachment.go b/internal/entities/repository_attachment.go new file mode 100644 index 0000000..faf4f96 --- /dev/null +++ b/internal/entities/repository_attachment.go @@ -0,0 +1,19 @@ +package entities + +import ( + "time" + + "github.com/google/uuid" +) + +type RepositoryAttachment struct { + ID uuid.UUID `gorm:"type:uuid;primary_key;default:gen_random_uuid()" json:"id"` + FileURL string `gorm:"not null" json:"file_url"` + FileName string `gorm:"not null" json:"file_name"` + FileType string `gorm:"not null" json:"file_type"` + Category string `gorm:"not null" json:"category"` + UploadedBy *uuid.UUID `json:"uploaded_by,omitempty"` + UploadedAt time.Time `gorm:"autoCreateTime" json:"uploaded_at"` +} + +func (RepositoryAttachment) TableName() string { return "repository_attachments" } diff --git a/internal/handler/repository_attachment_handler.go b/internal/handler/repository_attachment_handler.go new file mode 100644 index 0000000..9079155 --- /dev/null +++ b/internal/handler/repository_attachment_handler.go @@ -0,0 +1,137 @@ +package handler + +import ( + "eslogad-be/internal/constants" + "eslogad-be/internal/contract" + "eslogad-be/internal/logger" + "net/http" + "strconv" + + "github.com/gin-gonic/gin" + "github.com/google/uuid" +) + +type RepositoryAttachmentHandler struct { + attachmentService RepositoryAttachmentService +} + +func NewRepositoryAttachmentHandler(attachmentService RepositoryAttachmentService) *RepositoryAttachmentHandler { + return &RepositoryAttachmentHandler{ + attachmentService: attachmentService, + } +} + +func (h *RepositoryAttachmentHandler) CreateAttachment(c *gin.Context) { + var req contract.CreateRepositoryAttachmentRequest + if err := c.ShouldBindJSON(&req); err != nil { + logger.FromContext(c).WithError(err).Error("UserHandler::CreateAttachment -> request binding failed") + h.sendValidationErrorResponse(c, "Invalid request body", constants.MissingFieldErrorCode) + return + } + + userResponse, err := h.attachmentService.CreateAttachment(c.Request.Context(), &req) + if err != nil { + logger.FromContext(c).WithError(err).Error("UserHandler::CreateAttachment -> Failed to create user from service") + h.sendErrorResponse(c, err.Error(), http.StatusInternalServerError) + return + } + + logger.FromContext(c).Infof("UserHandler::CreateUser -> Successfully created repository attachment = %+v", userResponse) + c.JSON(http.StatusOK, contract.BuildSuccessResponse(userResponse)) +} + +func (h *RepositoryAttachmentHandler) DeleteAttachment(c *gin.Context) { + attachmentIDStr := c.Param("id") + attachmentID, err := uuid.Parse(attachmentIDStr) + if err != nil { + logger.FromContext(c).WithError(err).Error("UserHandler::DeleteAttachment -> Invalid attachment id") + h.sendValidationErrorResponse(c, "Invalid user ID", constants.MalformedFieldErrorCode) + return + } + + err = h.attachmentService.DeleteAttachment(c.Request.Context(), attachmentID) + if err != nil { + logger.FromContext(c).WithError(err).Error("UserHandler::DeleteAttachment -> Failed to delete attachment from service") + h.sendErrorResponse(c, err.Error(), http.StatusInternalServerError) + return + } + + logger.FromContext(c).Info("UserHandler::DeleteAttachment -> Successfully deleted attachment") + c.JSON(http.StatusOK, &contract.SuccessResponse{Message: "User deleted successfully"}) +} + +func (h *RepositoryAttachmentHandler) GetAttachment(c *gin.Context) { + attachmentIDStr := c.Param("id") + attachmentID, err := uuid.Parse(attachmentIDStr) + if err != nil { + logger.FromContext(c).WithError(err).Error("UserHandler::GetAttachment -> Invalid attachment ID") + h.sendValidationErrorResponse(c, "Invalid user ID", constants.MalformedFieldErrorCode) + return + } + + attachmentResponse, err := h.attachmentService.GetById(c.Request.Context(), attachmentID) + if err != nil { + logger.FromContext(c).WithError(err).Error("UserHandler::GetAttachment -> Failed to get attachment from service") + h.sendErrorResponse(c, err.Error(), http.StatusInternalServerError) + return + } + + logger.FromContext(c).Infof("UserHandler::GetAttachment -> Successfully retrieved attachment = %+v", attachmentResponse) + c.JSON(http.StatusOK, attachmentResponse) +} +func (h *RepositoryAttachmentHandler) ListAttachment(c *gin.Context) { + ctx := c.Request.Context() + + req := &contract.ListRepositoryAttachmentsRequest{ + Page: 1, + Limit: 10, + } + + if page := c.Query("page"); page != "" { + if p, err := strconv.Atoi(page); err == nil { + req.Page = p + } + } + + if limit := c.Query("limit"); limit != "" { + if l, err := strconv.Atoi(limit); err == nil { + req.Limit = l + } + } + + attachmentsResponse, err := h.attachmentService.ListAttachment(ctx, req) + if err != nil { + logger.FromContext(c).WithError(err).Error("UserHandler::ListUsers -> Failed to list users from service") + h.sendErrorResponse(c, err.Error(), http.StatusInternalServerError) + return + } + + logger.FromContext(c).Infof("UserHandler::ListUsers -> Successfully listed users = %+v", attachmentsResponse) + c.JSON(http.StatusOK, contract.BuildSuccessResponse(attachmentsResponse)) +} + +func (h *RepositoryAttachmentHandler) sendValidationErrorResponse(c *gin.Context, message string, errorCode string) { + statusCode := constants.HttpErrorMap[errorCode] + if statusCode == 0 { + statusCode = http.StatusBadRequest + } + + errorResponse := &contract.ErrorResponse{ + Error: message, + Code: statusCode, + Details: map[string]interface{}{ + "error_code": errorCode, + "entity": constants.UserValidatorEntity, + }, + } + c.JSON(statusCode, errorResponse) +} + +func (h *RepositoryAttachmentHandler) sendErrorResponse(c *gin.Context, message string, statusCode int) { + errorResponse := &contract.ErrorResponse{ + Error: message, + Code: statusCode, + Details: map[string]interface{}{}, + } + c.JSON(statusCode, errorResponse) +} diff --git a/internal/handler/repository_attachment_service.go b/internal/handler/repository_attachment_service.go new file mode 100644 index 0000000..52d397e --- /dev/null +++ b/internal/handler/repository_attachment_service.go @@ -0,0 +1,15 @@ +package handler + +import ( + "context" + "eslogad-be/internal/contract" + + "github.com/google/uuid" +) + +type RepositoryAttachmentService interface { + CreateAttachment(ctx context.Context, req *contract.CreateRepositoryAttachmentRequest) (*contract.RepositoryAttachmentsResponse, error) + DeleteAttachment(ctx context.Context, id uuid.UUID) error + GetById(ctx context.Context, id uuid.UUID) (*contract.RepositoryAttachmentsResponse, error) + ListAttachment(ctx context.Context, req *contract.ListRepositoryAttachmentsRequest) (*contract.ListRepositoryAttachmentsResponse, error) +} diff --git a/internal/processor/repository_attachment_processor.go b/internal/processor/repository_attachment_processor.go new file mode 100644 index 0000000..5432660 --- /dev/null +++ b/internal/processor/repository_attachment_processor.go @@ -0,0 +1,78 @@ +package processor + +import ( + "context" + "eslogad-be/internal/appcontext" + "eslogad-be/internal/contract" + "eslogad-be/internal/transformer" + "fmt" + + "github.com/google/uuid" +) + +type RepositoryAttachmentProcessorImpl struct { + attachmentRepo RepositoryAttachmentRepository +} + +func NewRepositoryAttachmentProcessor(attachmentRepo RepositoryAttachmentRepository) *RepositoryAttachmentProcessorImpl { + return &RepositoryAttachmentProcessorImpl{ + attachmentRepo: attachmentRepo, + } +} + +func (p *RepositoryAttachmentProcessorImpl) CreateAttachment(ctx context.Context, req *contract.CreateRepositoryAttachmentRequest) (*contract.RepositoryAttachmentsResponse, error) { + userID := getUserIDFromContext(ctx) + + attachmentEntity := transformer.CreateRepositoryAttachmentRequestToEntity(req, userID) + + err := p.attachmentRepo.Create(ctx, attachmentEntity) + if err != nil { + return nil, fmt.Errorf("failed to create repository attachment: %w", err) + } + + return transformer.RepositoryAttachmentEntityToContract(attachmentEntity), nil +} + +func getUserIDFromContext(ctx context.Context) uuid.UUID { + appCtx := appcontext.FromGinContext(ctx) + if appCtx != nil { + return appCtx.UserID + } + return uuid.New() +} + +func (p *RepositoryAttachmentProcessorImpl) DeleteAttachment(ctx context.Context, id uuid.UUID) error { + _, err := p.attachmentRepo.GetByID(ctx, id) + if err != nil { + return fmt.Errorf("repository attachment not found: %w", err) + } + + err = p.attachmentRepo.Delete(ctx, id) + if err != nil { + return fmt.Errorf("failed to delete repository attachment: %w", err) + } + + return nil +} + +func (p *RepositoryAttachmentProcessorImpl) GetById(ctx context.Context, id uuid.UUID) (*contract.RepositoryAttachmentsResponse, error) { + attachment, err := p.attachmentRepo.GetByID(ctx, id) + if err != nil { + return nil, fmt.Errorf("repository attachment not found: %w", err) + } + + resp := transformer.RepositoryAttachmentEntityToContract(attachment) + + return resp, nil +} + +func (p *RepositoryAttachmentProcessorImpl) ListAttachment(ctx context.Context, search *string, limit, offset int) ([]contract.RepositoryAttachmentsResponse, int, error) { + attachments, totalCount, err := p.attachmentRepo.List(ctx, search, limit, offset) + if err != nil { + return nil, 0, fmt.Errorf("failed to get users: %w", err) + } + + responses := transformer.RepositoryAttachmentEntityToContracts(attachments) + + return responses, int(totalCount), nil +} diff --git a/internal/processor/repository_attachment_repository.go b/internal/processor/repository_attachment_repository.go new file mode 100644 index 0000000..8b29edf --- /dev/null +++ b/internal/processor/repository_attachment_repository.go @@ -0,0 +1,16 @@ +package processor + +import ( + "context" + "eslogad-be/internal/entities" + + "github.com/google/uuid" +) + +type RepositoryAttachmentRepository interface { + Create(ctx context.Context, user *entities.RepositoryAttachment) error + GetByID(ctx context.Context, id uuid.UUID) (*entities.RepositoryAttachment, error) + Update(ctx context.Context, user *entities.RepositoryAttachment) error + Delete(ctx context.Context, id uuid.UUID) error + List(ctx context.Context, search *string, limit, offset int) ([]*entities.RepositoryAttachment, int64, error) +} diff --git a/internal/repository/repository_attachment_repository.go b/internal/repository/repository_attachment_repository.go new file mode 100644 index 0000000..6ece0bf --- /dev/null +++ b/internal/repository/repository_attachment_repository.go @@ -0,0 +1,74 @@ +package repository + +import ( + "context" + "eslogad-be/internal/entities" + + "github.com/google/uuid" + "gorm.io/gorm" +) + +type RepositoryAttachmentRepositoryImpl struct { + b *gorm.DB +} + +func NewRepositoryAttachmentRepositoryImpl(db *gorm.DB) *RepositoryAttachmentRepositoryImpl { + return &RepositoryAttachmentRepositoryImpl{ + b: db, + } +} + +func (r *RepositoryAttachmentRepositoryImpl) Create(ctx context.Context, user *entities.RepositoryAttachment) error { + return r.b.WithContext(ctx).Create(user).Error +} + +func (r *RepositoryAttachmentRepositoryImpl) GetByID(ctx context.Context, id uuid.UUID) (*entities.RepositoryAttachment, error) { + var attachment entities.RepositoryAttachment + err := r.b.WithContext(ctx). + First(&attachment, "id = ?", id).Error + if err != nil { + return nil, err + } + return &attachment, nil +} + +func (r *RepositoryAttachmentRepositoryImpl) Update(ctx context.Context, user *entities.RepositoryAttachment) error { + return r.b.WithContext(ctx).Save(user).Error +} + +func (r *RepositoryAttachmentRepositoryImpl) Delete(ctx context.Context, id uuid.UUID) error { + return r.b.WithContext(ctx).Delete(&entities.RepositoryAttachment{}, "id = ?", id).Error +} + +func (r *RepositoryAttachmentRepositoryImpl) List(ctx context.Context, search *string, limit, offset int) ([]*entities.RepositoryAttachment, int64, error) { + var attachments []*entities.RepositoryAttachment + var total int64 + + baseQuery := r.b.WithContext(ctx).Model(&entities.RepositoryAttachment{}) + + if search != nil && *search != "" { + like := "%" + *search + "%" + baseQuery = baseQuery.Where("name ILIKE ? OR email ILIKE ?", like, like) + } + + countQuery := baseQuery + if err := countQuery.Count(&total).Error; err != nil { + return nil, 0, err + } + + dataQuery := r.b.WithContext(ctx).Model(&entities.RepositoryAttachment{}) + + if search != nil && *search != "" { + like := "%" + *search + "%" + dataQuery = dataQuery.Where("name ILIKE ? OR category ILIKE ?", like, like) + } + + if err := dataQuery. + Limit(limit). + Offset(offset). + Find(&attachments).Error; err != nil { + return nil, 0, err + } + + return attachments, total, nil +} diff --git a/internal/router/health_handler.go b/internal/router/health_handler.go index 0a15b18..35be88a 100644 --- a/internal/router/health_handler.go +++ b/internal/router/health_handler.go @@ -180,3 +180,10 @@ type NotificationHandler interface { GetCurrentUserSubscriber(c *gin.Context) UpdateCurrentUserSubscriberChannel(c *gin.Context) } + +type RepositoryAttachmentHandler interface { + CreateAttachment(c *gin.Context) + DeleteAttachment(c *gin.Context) + GetAttachment(c *gin.Context) + ListAttachment(c *gin.Context) +} diff --git a/internal/router/router.go b/internal/router/router.go index 92c3bff..ff353c7 100644 --- a/internal/router/router.go +++ b/internal/router/router.go @@ -8,21 +8,22 @@ import ( ) type Router struct { - config *config.Config - authHandler AuthHandler - healthHandler HealthHandler - authMiddleware AuthMiddleware - userHandler UserHandler - fileHandler FileHandler - rbacHandler RBACHandler - masterHandler MasterHandler - letterHandler LetterHandler - letterOutgoingHandler LetterOutgoingHandler - adminApprovalFlowHandler AdminApprovalFlowHandler - dispRouteHandler DispositionRouteHandler - onlyOfficeHandler OnlyOfficeHandler - analyticsHandler AnalyticsHandler - notificationHandler NotificationHandler + config *config.Config + authHandler AuthHandler + healthHandler HealthHandler + authMiddleware AuthMiddleware + userHandler UserHandler + fileHandler FileHandler + rbacHandler RBACHandler + masterHandler MasterHandler + letterHandler LetterHandler + letterOutgoingHandler LetterOutgoingHandler + adminApprovalFlowHandler AdminApprovalFlowHandler + dispRouteHandler DispositionRouteHandler + onlyOfficeHandler OnlyOfficeHandler + analyticsHandler AnalyticsHandler + notificationHandler NotificationHandler + repositoryAttachmentHandler RepositoryAttachmentHandler } func NewRouter( @@ -41,23 +42,25 @@ func NewRouter( onlyOfficeHandler OnlyOfficeHandler, analyticsHandler AnalyticsHandler, notificationHandler NotificationHandler, + repositoryAttachmentHandler RepositoryAttachmentHandler, ) *Router { return &Router{ - config: cfg, - authHandler: authHandler, - authMiddleware: authMiddleware, - healthHandler: healthHandler, - userHandler: userHandler, - fileHandler: fileHandler, - rbacHandler: rbacHandler, - masterHandler: masterHandler, - letterHandler: letterHandler, - letterOutgoingHandler: letterOutgoingHandler, - adminApprovalFlowHandler: adminApprovalFlowHandler, - dispRouteHandler: dispRouteHandler, - onlyOfficeHandler: onlyOfficeHandler, - analyticsHandler: analyticsHandler, - notificationHandler: notificationHandler, + config: cfg, + authHandler: authHandler, + authMiddleware: authMiddleware, + healthHandler: healthHandler, + userHandler: userHandler, + fileHandler: fileHandler, + rbacHandler: rbacHandler, + masterHandler: masterHandler, + letterHandler: letterHandler, + letterOutgoingHandler: letterOutgoingHandler, + adminApprovalFlowHandler: adminApprovalFlowHandler, + dispRouteHandler: dispRouteHandler, + onlyOfficeHandler: onlyOfficeHandler, + analyticsHandler: analyticsHandler, + notificationHandler: notificationHandler, + repositoryAttachmentHandler: repositoryAttachmentHandler, } } @@ -236,6 +239,15 @@ func (r *Router) addAppRoutes(rg *gin.Engine) { droutes.PUT("/:id/active", r.dispRouteHandler.SetActive) } + repoattachsch := v1.Group("/repository-attachments") + repoattachsch.Use(r.authMiddleware.RequireAuth()) + { + repoattachsch.POST("", r.repositoryAttachmentHandler.CreateAttachment) + repoattachsch.GET("", r.repositoryAttachmentHandler.ListAttachment) + repoattachsch.DELETE("/:id", r.repositoryAttachmentHandler.DeleteAttachment) + repoattachsch.GET("/:id", r.repositoryAttachmentHandler.GetAttachment) + } + admin := v1.Group("/setting") admin.Use(r.authMiddleware.RequireAuth()) { diff --git a/internal/service/repository_attachment_processor.go b/internal/service/repository_attachment_processor.go new file mode 100644 index 0000000..6f88baa --- /dev/null +++ b/internal/service/repository_attachment_processor.go @@ -0,0 +1,15 @@ +package service + +import ( + "context" + "eslogad-be/internal/contract" + + "github.com/google/uuid" +) + +type RepositoryAttachmentProcessor interface { + CreateAttachment(ctx context.Context, req *contract.CreateRepositoryAttachmentRequest) (*contract.RepositoryAttachmentsResponse, error) + DeleteAttachment(ctx context.Context, id uuid.UUID) error + GetById(ctx context.Context, id uuid.UUID) (*contract.RepositoryAttachmentsResponse, error) + ListAttachment(ctx context.Context, search *string, limit, offset int) ([]contract.RepositoryAttachmentsResponse, int, error) +} diff --git a/internal/service/repository_attachment_service.go b/internal/service/repository_attachment_service.go new file mode 100644 index 0000000..9c96839 --- /dev/null +++ b/internal/service/repository_attachment_service.go @@ -0,0 +1,59 @@ +package service + +import ( + "context" + "eslogad-be/internal/contract" + "eslogad-be/internal/transformer" + + "github.com/google/uuid" +) + +type RepositoryAttachmentServiceImpl struct { + attachmentProcessor RepositoryAttachmentProcessor +} + +func NewRepositoryAttachmentService(attachmentProcessor RepositoryAttachmentProcessor) *RepositoryAttachmentServiceImpl { + return &RepositoryAttachmentServiceImpl{ + attachmentProcessor: attachmentProcessor, + } +} + +func (s *RepositoryAttachmentServiceImpl) CreateAttachment(ctx context.Context, req *contract.CreateRepositoryAttachmentRequest) (*contract.RepositoryAttachmentsResponse, error) { + return s.attachmentProcessor.CreateAttachment(ctx, req) +} + +func (s *RepositoryAttachmentServiceImpl) DeleteAttachment(ctx context.Context, id uuid.UUID) error { + return s.attachmentProcessor.DeleteAttachment(ctx, id) +} + +func (s *RepositoryAttachmentServiceImpl) GetById(ctx context.Context, id uuid.UUID) (*contract.RepositoryAttachmentsResponse, error) { + return s.attachmentProcessor.GetById(ctx, id) +} + +func (s *RepositoryAttachmentServiceImpl) ListAttachment(ctx context.Context, req *contract.ListRepositoryAttachmentsRequest) (*contract.ListRepositoryAttachmentsResponse, error) { + page := req.Page + if page <= 0 { + page = 1 + } + + limit := req.Limit + if limit <= 0 { + limit = 10 + } + if limit > 100 { + limit = 100 // Max limit to prevent performance issues + } + + offset := (page - 1) * limit + + // Pass calculated offset and limit to processor + attachmentResponses, totalCount, err := s.attachmentProcessor.ListAttachment(ctx, req.Search, limit, offset) + if err != nil { + return nil, err + } + + return &contract.ListRepositoryAttachmentsResponse{ + Attachments: attachmentResponses, + Pagination: transformer.CreatePaginationResponse(totalCount, page, limit), + }, nil +} diff --git a/internal/transformer/repository_attachment_transformer.go b/internal/transformer/repository_attachment_transformer.go new file mode 100644 index 0000000..7f5386f --- /dev/null +++ b/internal/transformer/repository_attachment_transformer.go @@ -0,0 +1,49 @@ +package transformer + +import ( + "eslogad-be/internal/contract" + "eslogad-be/internal/entities" + + "github.com/google/uuid" +) + +func CreateRepositoryAttachmentRequestToEntity(req *contract.CreateRepositoryAttachmentRequest, userId uuid.UUID) *entities.RepositoryAttachment { + if req == nil { + return nil + } + return &entities.RepositoryAttachment{ + FileName: req.FileName, + FileType: req.FileType, + FileURL: req.FileURL, + UploadedBy: &userId, + Category: req.Category, + } +} + +func RepositoryAttachmentEntityToContract(entity *entities.RepositoryAttachment) *contract.RepositoryAttachmentsResponse { + resp := &contract.RepositoryAttachmentsResponse{ + ID: entity.ID, + FileName: entity.FileName, + FileType: entity.FileType, + FileURL: entity.FileURL, + Category: entity.Category, + UploadBy: *entity.UploadedBy, + UploadAt: entity.UploadedAt, + } + + return resp +} + +func RepositoryAttachmentEntityToContracts(attachments []*entities.RepositoryAttachment) []contract.RepositoryAttachmentsResponse { + if attachments == nil { + return nil + } + responses := make([]contract.RepositoryAttachmentsResponse, len(attachments)) + for i, u := range attachments { + resp := RepositoryAttachmentEntityToContract(u) + if resp != nil { + responses[i] = *resp + } + } + return responses +} diff --git a/migrations/000044_create_repository_attachments_table.down.sql b/migrations/000044_create_repository_attachments_table.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/migrations/000044_create_repository_attachments_table.up.sql b/migrations/000044_create_repository_attachments_table.up.sql new file mode 100644 index 0000000..49e69f0 --- /dev/null +++ b/migrations/000044_create_repository_attachments_table.up.sql @@ -0,0 +1,13 @@ +BEGIN; + +CREATE TABLE IF NOT EXISTS repository_attachments ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + file_url TEXT NOT NULL, + file_name TEXT NOT NULL, + file_type TEXT NOT NULL, + category TEXT NOT NULL, + uploaded_by UUID REFERENCES users(id) ON DELETE SET NULL, + uploaded_at TIMESTAMP WITHOUT TIME ZONE DEFAULT CURRENT_TIMESTAMP + ); + +COMMIT; \ No newline at end of file