From 094a1474892fdcfd728a607220380e4c3f81b1ac Mon Sep 17 00:00:00 2001 From: pahmiudahgede Date: Wed, 9 Jul 2025 14:14:42 +0700 Subject: [PATCH] feat: admin view approval needed --- internal/admin/approval_dto.go | 144 ++++++++ internal/admin/approval_handler.go | 293 ++++++++++++++++ internal/admin/approval_repository.go | 183 ++++++++++ internal/admin/approval_route.go | 25 ++ internal/admin/approval_service.go | 320 ++++++++++++++++++ .../authentication/authentication_service.go | 4 +- internal/chat/model/chat_model.go | 1 - internal/userprofile/userprofile_service.go | 2 - model/user_model.go | 36 +- router/setup_routes.go.go | 2 + 10 files changed, 988 insertions(+), 22 deletions(-) create mode 100644 internal/admin/approval_dto.go create mode 100644 internal/admin/approval_handler.go create mode 100644 internal/admin/approval_repository.go create mode 100644 internal/admin/approval_route.go create mode 100644 internal/admin/approval_service.go delete mode 100644 internal/chat/model/chat_model.go diff --git a/internal/admin/approval_dto.go b/internal/admin/approval_dto.go new file mode 100644 index 0000000..dc1fd26 --- /dev/null +++ b/internal/admin/approval_dto.go @@ -0,0 +1,144 @@ +package admin + +import "time" + +// Request DTOs +type GetPendingUsersRequest struct { + Role string `query:"role" validate:"omitempty,oneof=pengelola pengepul"` + Status string `query:"status" validate:"omitempty,oneof=awaiting_approval pending"` + Page int `query:"page" validate:"min=1"` + Limit int `query:"limit" validate:"min=1,max=100"` +} + +type ApprovalActionRequest struct { + UserID string `json:"user_id" validate:"required,uuid"` + Action string `json:"action" validate:"required,oneof=approve reject"` + Notes string `json:"notes" validate:"omitempty,max=500"` +} + +type BulkApprovalRequest struct { + UserIDs []string `json:"user_ids" validate:"required,min=1,max=50,dive,uuid"` + Action string `json:"action" validate:"required,oneof=approve reject"` + Notes string `json:"notes" validate:"omitempty,max=500"` +} + +// Response DTOs +type PendingUserResponse struct { + ID string `json:"id"` + Name string `json:"name,omitempty"` + Phone string `json:"phone"` + Email string `json:"email,omitempty"` + Role RoleInfo `json:"role"` + RegistrationStatus string `json:"registration_status"` + RegistrationProgress int8 `json:"registration_progress"` + SubmittedAt time.Time `json:"submitted_at"` + IdentityCard *IdentityCardInfo `json:"identity_card,omitempty"` + CompanyProfile *CompanyProfileInfo `json:"company_profile,omitempty"` + RegistrationStepInfo *RegistrationStepResponse `json:"step_info"` +} + +type RoleInfo struct { + ID string `json:"id"` + RoleName string `json:"role_name"` +} + +type IdentityCardInfo struct { + ID string `json:"id"` + IdentificationNumber string `json:"identification_number"` + Fullname string `json:"fullname"` + Placeofbirth string `json:"place_of_birth"` + Dateofbirth string `json:"date_of_birth"` + Gender string `json:"gender"` + BloodType string `json:"blood_type"` + Province string `json:"province"` + District string `json:"district"` + SubDistrict string `json:"sub_district"` + Village string `json:"village"` + PostalCode string `json:"postal_code"` + Religion string `json:"religion"` + Maritalstatus string `json:"marital_status"` + Job string `json:"job"` + Citizenship string `json:"citizenship"` + Validuntil string `json:"valid_until"` + Cardphoto string `json:"card_photo"` +} + +type CompanyProfileInfo struct { + ID string `json:"id"` + CompanyName string `json:"company_name"` + CompanyAddress string `json:"company_address"` + CompanyPhone string `json:"company_phone"` + CompanyEmail string `json:"company_email"` + CompanyLogo string `json:"company_logo"` + CompanyWebsite string `json:"company_website"` + TaxID string `json:"tax_id"` + FoundedDate string `json:"founded_date"` + CompanyType string `json:"company_type"` + CompanyDescription string `json:"company_description"` +} + +type RegistrationStepResponse struct { + Step int `json:"step"` + Status string `json:"status"` + Description string `json:"description"` + RequiresAdminApproval bool `json:"requires_admin_approval"` + IsAccessible bool `json:"is_accessible"` + IsCompleted bool `json:"is_completed"` +} + +type PendingUsersListResponse struct { + Users []PendingUserResponse `json:"users"` + Pagination PaginationInfo `json:"pagination"` + Summary ApprovalSummary `json:"summary"` +} + +type PaginationInfo struct { + Page int `json:"page"` + Limit int `json:"limit"` + TotalPages int `json:"total_pages"` + TotalRecords int64 `json:"total_records"` + HasNext bool `json:"has_next"` + HasPrev bool `json:"has_prev"` +} + +type ApprovalSummary struct { + TotalPending int64 `json:"total_pending"` + PengelolaPending int64 `json:"pengelola_pending"` + PengepulPending int64 `json:"pengepul_pending"` +} + +type ApprovalActionResponse struct { + UserID string `json:"user_id"` + Action string `json:"action"` + PreviousStatus string `json:"previous_status"` + NewStatus string `json:"new_status"` + ProcessedAt time.Time `json:"processed_at"` + ProcessedBy string `json:"processed_by"` + Notes string `json:"notes,omitempty"` +} + +type BulkApprovalResponse struct { + SuccessCount int `json:"success_count"` + FailureCount int `json:"failure_count"` + Results []ApprovalActionResponse `json:"results"` + Failures []ApprovalFailure `json:"failures,omitempty"` +} + +type ApprovalFailure struct { + UserID string `json:"user_id"` + Error string `json:"error"` + Reason string `json:"reason"` +} + +// Validation helper +func (r *GetPendingUsersRequest) SetDefaults() { + if r.Page <= 0 { + r.Page = 1 + } + if r.Limit <= 0 { + r.Limit = 20 + } + if r.Status == "" { + r.Status = "awaiting_approval" + } +} diff --git a/internal/admin/approval_handler.go b/internal/admin/approval_handler.go new file mode 100644 index 0000000..4f7fc81 --- /dev/null +++ b/internal/admin/approval_handler.go @@ -0,0 +1,293 @@ +// internal/admin/approval_handler.go +package admin + +import ( + "rijig/middleware" + "rijig/utils" + + "github.com/gofiber/fiber/v2" +) + +type ApprovalHandler struct { + service ApprovalService +} + +func NewApprovalHandler(service ApprovalService) *ApprovalHandler { + return &ApprovalHandler{ + service: service, + } +} + +// GetPendingUsers menampilkan daftar pengguna yang menunggu persetujuan +// @Summary Get pending users for approval +// @Description Retrieve list of users (pengelola/pengepul) waiting for admin approval with filtering and pagination +// @Tags Admin - User Approval +// @Accept json +// @Produce json +// @Param role query string false "Filter by role" Enums(pengelola, pengepul) +// @Param status query string false "Filter by status" Enums(awaiting_approval, pending) default(awaiting_approval) +// @Param page query int false "Page number" default(1) minimum(1) +// @Param limit query int false "Items per page" default(20) minimum(1) maximum(100) +// @Success 200 {object} utils.Response{data=PendingUsersListResponse} "List of pending users" +// @Failure 400 {object} utils.Response "Bad request" +// @Failure 401 {object} utils.Response "Unauthorized" +// @Failure 403 {object} utils.Response "Forbidden - Admin role required" +// @Failure 500 {object} utils.Response "Internal server error" +// @Security Bearer +// @Router /admin/users/pending [get] +func (h *ApprovalHandler) GetPendingUsers(c *fiber.Ctx) error { + // Parse query parameters + var req GetPendingUsersRequest + if err := c.QueryParser(&req); err != nil { + return utils.BadRequest(c, "Invalid query parameters: "+err.Error()) + } + + // Validate request + // if err := h.validator.Struct(&req); err != nil { + // return utils.ResponseErrorData(c, fiber.StatusBadRequest, "Validation failed", err.Error()) + // } + + // Call service + result, err := h.service.GetPendingUsers(c.Context(), &req) + if err != nil { + return utils.InternalServerError(c, "Failed to get pending users: "+err.Error()) + } + + return utils.SuccessWithData(c, "Pending users retrieved successfully", result) +} + +// GetUserApprovalDetails menampilkan detail lengkap pengguna untuk approval +// @Summary Get user approval details +// @Description Get detailed information of a specific user for approval decision +// @Tags Admin - User Approval +// @Accept json +// @Produce json +// @Param user_id path string true "User ID" format(uuid) +// @Success 200 {object} utils.Response{data=PendingUserResponse} "User approval details" +// @Failure 400 {object} utils.Response "Bad request" +// @Failure 401 {object} utils.Response "Unauthorized" +// @Failure 403 {object} utils.Response "Forbidden - Admin role required" +// @Failure 404 {object} utils.Response "User not found" +// @Failure 500 {object} utils.Response "Internal server error" +// @Security Bearer +// @Router /admin/users/{user_id}/approval-details [get] +func (h *ApprovalHandler) GetUserApprovalDetails(c *fiber.Ctx) error { + userID := c.Params("user_id") + if userID == "" { + return utils.BadRequest(c, "User ID is required") + } + + // Validate UUID format + // if err := h.validator.Var(userID, "uuid"); err != nil { + // return utils.BadRequest(c, "Invalid user ID format") + // } + + // Call service + result, err := h.service.GetUserApprovalDetails(c.Context(), userID) + if err != nil { + if err.Error() == "user not found" { + return utils.NotFound(c, "User not found") + } + return utils.InternalServerError(c, "Failed to get user details: "+err.Error()) + } + + return utils.SuccessWithData(c, "User approval details retrieved successfully", result) +} + +// ProcessApprovalAction memproses aksi approval (approve/reject) untuk satu user +// @Summary Process approval action +// @Description Approve or reject a user registration +// @Tags Admin - User Approval +// @Accept json +// @Produce json +// @Param request body ApprovalActionRequest true "Approval action request" +// @Success 200 {object} utils.Response{data=ApprovalActionResponse} "Approval processed successfully" +// @Failure 400 {object} utils.Response "Bad request" +// @Failure 401 {object} utils.Response "Unauthorized" +// @Failure 403 {object} utils.Response "Forbidden - Admin role required" +// @Failure 404 {object} utils.Response "User not found" +// @Failure 500 {object} utils.Response "Internal server error" +// @Security Bearer +// @Router /admin/users/approval-action [post] +func (h *ApprovalHandler) ProcessApprovalAction(c *fiber.Ctx) error { + // Get admin ID from context + adminClaims, err := middleware.GetUserFromContext(c) + if err != nil { + return utils.Unauthorized(c, "Admin authentication required") + } + + // Parse request body + var req ApprovalActionRequest + if err := c.BodyParser(&req); err != nil { + return utils.BadRequest(c, "Invalid request body: "+err.Error()) + } + + // Validate request + // if err := h.validator.Struct(&req); err != nil { + // return utils.ResponseErrorData(c, fiber.StatusBadRequest, "Validation failed", err.Error()) + // } + + // Call service + result, err := h.service.ProcessApprovalAction(c.Context(), &req, adminClaims.UserID) + if err != nil { + if err.Error() == "user not found" { + return utils.NotFound(c, "User not found") + } + return utils.InternalServerError(c, "Failed to process approval: "+err.Error()) + } + + actionMessage := "User approved successfully" + if req.Action == "reject" { + actionMessage = "User rejected successfully" + } + + return utils.SuccessWithData(c, actionMessage, result) +} + +// BulkProcessApproval memproses aksi approval untuk multiple users sekaligus +// @Summary Bulk process approval actions +// @Description Approve or reject multiple users at once +// @Tags Admin - User Approval +// @Accept json +// @Produce json +// @Param request body BulkApprovalRequest true "Bulk approval request" +// @Success 200 {object} utils.Response{data=BulkApprovalResponse} "Bulk approval processed" +// @Failure 400 {object} utils.Response "Bad request" +// @Failure 401 {object} utils.Response "Unauthorized" +// @Failure 403 {object} utils.Response "Forbidden - Admin role required" +// @Failure 500 {object} utils.Response "Internal server error" +// @Security Bearer +// @Router /admin/users/bulk-approval [post] +func (h *ApprovalHandler) BulkProcessApproval(c *fiber.Ctx) error { + // Get admin ID from context + adminClaims, err := middleware.GetUserFromContext(c) + if err != nil { + return utils.Unauthorized(c, "Admin authentication required") + } + + // Parse request body + var req BulkApprovalRequest + if err := c.BodyParser(&req); err != nil { + return utils.BadRequest(c, "Invalid request body: "+err.Error()) + } + + // Validate request + // if err := h.validator.Struct(&req); err != nil { + // return utils.ResponseErrorData(c, fiber.StatusBadRequest, "Validation failed", err.Error()) + // } + + // Call service + result, err := h.service.BulkProcessApproval(c.Context(), &req, adminClaims.UserID) + if err != nil { + return utils.InternalServerError(c, "Failed to process bulk approval: "+err.Error()) + } + + actionMessage := "Bulk approval processed successfully" + if req.Action == "reject" { + actionMessage = "Bulk rejection processed successfully" + } + + return utils.SuccessWithData(c, actionMessage, result) +} + +// ApproveUser endpoint khusus untuk approve satu user (shortcut) +// @Summary Approve user +// @Description Approve a user registration (shortcut endpoint) +// @Tags Admin - User Approval +// @Accept json +// @Produce json +// @Param user_id path string true "User ID" format(uuid) +// @Param notes body string false "Optional approval notes" +// @Success 200 {object} utils.Response{data=ApprovalActionResponse} "User approved successfully" +// @Failure 400 {object} utils.Response "Bad request" +// @Failure 401 {object} utils.Response "Unauthorized" +// @Failure 403 {object} utils.Response "Forbidden - Admin role required" +// @Failure 404 {object} utils.Response "User not found" +// @Failure 500 {object} utils.Response "Internal server error" +// @Security Bearer +// @Router /admin/users/{user_id}/approve [post] +func (h *ApprovalHandler) ApproveUser(c *fiber.Ctx) error { + userID := c.Params("user_id") + if userID == "" { + return utils.BadRequest(c, "User ID is required") + } + + // Validate UUID format + // if err := h.validator.Var(userID, "uuid"); err != nil { + // return utils.BadRequest(c, "Invalid user ID format") + // } + + // Get admin ID from context + adminClaims, err := middleware.GetUserFromContext(c) + if err != nil { + return utils.Unauthorized(c, "Admin authentication required") + } + + // Parse optional notes from body + var body struct { + Notes string `json:"notes"` + } + c.BodyParser(&body) // Ignore error as notes are optional + + // Call service + result, err := h.service.ApproveUser(c.Context(), userID, adminClaims.UserID, body.Notes) + if err != nil { + if err.Error() == "user not found" { + return utils.NotFound(c, "User not found") + } + return utils.InternalServerError(c, "Failed to approve user: "+err.Error()) + } + + return utils.SuccessWithData(c, "User approved successfully", result) +} + +// RejectUser endpoint khusus untuk reject satu user (shortcut) +// @Summary Reject user +// @Description Reject a user registration (shortcut endpoint) +// @Tags Admin - User Approval +// @Accept json +// @Produce json +// @Param user_id path string true "User ID" format(uuid) +// @Param notes body string false "Optional rejection notes" +// @Success 200 {object} utils.Response{data=ApprovalActionResponse} "User rejected successfully" +// @Failure 400 {object} utils.Response "Bad request" +// @Failure 401 {object} utils.Response "Unauthorized" +// @Failure 403 {object} utils.Response "Forbidden - Admin role required" +// @Failure 404 {object} utils.Response "User not found" +// @Failure 500 {object} utils.Response "Internal server error" +// @Security Bearer +// @Router /admin/users/{user_id}/reject [post] +func (h *ApprovalHandler) RejectUser(c *fiber.Ctx) error { + userID := c.Params("user_id") + if userID == "" { + return utils.BadRequest(c, "User ID is required") + } + + // Validate UUID format + // if err := h.validator.Var(userID, "uuid"); err != nil { + // return utils.BadRequest(c, "Invalid user ID format") + // } + + // Get admin ID from context + adminClaims, err := middleware.GetUserFromContext(c) + if err != nil { + return utils.Unauthorized(c, "Admin authentication required") + } + + // Parse optional notes from body + var body struct { + Notes string `json:"notes"` + } + c.BodyParser(&body) // Ignore error as notes are optional + + // Call service + result, err := h.service.RejectUser(c.Context(), userID, adminClaims.UserID, body.Notes) + if err != nil { + if err.Error() == "user not found" { + return utils.NotFound(c, "User not found") + } + return utils.InternalServerError(c, "Failed to reject user: "+err.Error()) + } + + return utils.SuccessWithData(c, "User rejected successfully", result) +} \ No newline at end of file diff --git a/internal/admin/approval_repository.go b/internal/admin/approval_repository.go new file mode 100644 index 0000000..81f929b --- /dev/null +++ b/internal/admin/approval_repository.go @@ -0,0 +1,183 @@ +package admin + +import ( + "context" + "fmt" + "rijig/model" + "strings" + + "gorm.io/gorm" +) + +type ApprovalRepository interface { + GetPendingUsers(ctx context.Context, req *GetPendingUsersRequest) ([]model.User, int64, error) + GetUserByID(ctx context.Context, userID string) (*model.User, error) + UpdateUserRegistrationStatus(ctx context.Context, userID, status string, progress int8) error + GetApprovalSummary(ctx context.Context) (*ApprovalSummary, error) + GetUsersByIDs(ctx context.Context, userIDs []string) ([]model.User, error) + BulkUpdateRegistrationStatus(ctx context.Context, userIDs []string, status string, progress int8) error +} + +type approvalRepository struct { + db *gorm.DB +} + +func NewApprovalRepository(db *gorm.DB) ApprovalRepository { + return &approvalRepository{ + db: db, + } +} + +func (r *approvalRepository) GetPendingUsers(ctx context.Context, req *GetPendingUsersRequest) ([]model.User, int64, error) { + var users []model.User + var totalRecords int64 + + query := r.db.WithContext(ctx).Model(&model.User{}). + Preload("Role"). + Preload("IdentityCard"). + Preload("CompanyProfile") + + query = r.applyFilters(query, req) + + if err := query.Count(&totalRecords).Error; err != nil { + return nil, 0, fmt.Errorf("failed to count pending users: %w", err) + } + + offset := (req.Page - 1) * req.Limit + if err := query. + Order("created_at DESC"). + Limit(req.Limit). + Offset(offset). + Find(&users).Error; err != nil { + return nil, 0, fmt.Errorf("failed to fetch pending users: %w", err) + } + + return users, totalRecords, nil +} + +func (r *approvalRepository) applyFilters(query *gorm.DB, req *GetPendingUsersRequest) *gorm.DB { + + if req.Status != "" { + + if req.Status == "pending" { + req.Status = "awaiting_approval" + } + query = query.Where("registration_status = ?", req.Status) + } + + if req.Role != "" { + + query = query.Joins("JOIN roles ON roles.id = users.role_id"). + Where("LOWER(roles.role_name) = ?", strings.ToLower(req.Role)) + } + + query = query.Where("registration_progress >= ?", 2) + + return query +} + +func (r *approvalRepository) GetUserByID(ctx context.Context, userID string) (*model.User, error) { + var user model.User + + if err := r.db.WithContext(ctx). + Preload("Role"). + Preload("IdentityCard"). + Preload("CompanyProfile"). + Where("id = ?", userID). + First(&user).Error; err != nil { + if err == gorm.ErrRecordNotFound { + return nil, fmt.Errorf("user not found") + } + return nil, fmt.Errorf("failed to get user: %w", err) + } + + return &user, nil +} + +func (r *approvalRepository) UpdateUserRegistrationStatus(ctx context.Context, userID, status string, progress int8) error { + result := r.db.WithContext(ctx). + Model(&model.User{}). + Where("id = ?", userID). + Updates(map[string]interface{}{ + "registration_status": status, + "registration_progress": progress, + "updated_at": "NOW()", + }) + + if result.Error != nil { + return fmt.Errorf("failed to update user registration status: %w", result.Error) + } + + if result.RowsAffected == 0 { + return fmt.Errorf("user not found") + } + + return nil +} + +func (r *approvalRepository) GetApprovalSummary(ctx context.Context) (*ApprovalSummary, error) { + var summary ApprovalSummary + + if err := r.db.WithContext(ctx). + Model(&model.User{}). + Where("registration_status = ? AND registration_progress >= ?", "awaiting_approval", 2). + Count(&summary.TotalPending).Error; err != nil { + return nil, fmt.Errorf("failed to count total pending users: %w", err) + } + + if err := r.db.WithContext(ctx). + Model(&model.User{}). + Joins("JOIN roles ON roles.id = users.role_id"). + Where("users.registration_status = ? AND users.registration_progress >= ? AND LOWER(roles.role_name) = ?", + "awaiting_approval", 2, "pengelola"). + Count(&summary.PengelolaPending).Error; err != nil { + return nil, fmt.Errorf("failed to count pengelola pending: %w", err) + } + + if err := r.db.WithContext(ctx). + Model(&model.User{}). + Joins("JOIN roles ON roles.id = users.role_id"). + Where("users.registration_status = ? AND users.registration_progress >= ? AND LOWER(roles.role_name) = ?", + "awaiting_approval", 2, "pengepul"). + Count(&summary.PengepulPending).Error; err != nil { + return nil, fmt.Errorf("failed to count pengepul pending: %w", err) + } + + return &summary, nil +} + +func (r *approvalRepository) GetUsersByIDs(ctx context.Context, userIDs []string) ([]model.User, error) { + var users []model.User + + if err := r.db.WithContext(ctx). + Preload("Role"). + Preload("IdentityCard"). + Preload("CompanyProfile"). + Where("id IN ?", userIDs). + Find(&users).Error; err != nil { + return nil, fmt.Errorf("failed to get users by IDs: %w", err) + } + + return users, nil +} + +func (r *approvalRepository) BulkUpdateRegistrationStatus(ctx context.Context, userIDs []string, status string, progress int8) error { + result := r.db.WithContext(ctx). + Model(&model.User{}). + Where("id IN ?", userIDs). + Updates(map[string]interface{}{ + "registration_status": status, + "registration_progress": progress, + "updated_at": "NOW()", + }) + + if result.Error != nil { + return fmt.Errorf("failed to bulk update registration status: %w", result.Error) + } + + if result.RowsAffected == 0 { + return fmt.Errorf("no users found to update") + } + + return nil +} diff --git a/internal/admin/approval_route.go b/internal/admin/approval_route.go new file mode 100644 index 0000000..48a6a4e --- /dev/null +++ b/internal/admin/approval_route.go @@ -0,0 +1,25 @@ +package admin + +import ( + "rijig/config" + "rijig/middleware" + + "github.com/gofiber/fiber/v2" +) + +func ApprovalRoutes(api fiber.Router) { + baseRepo := NewApprovalRepository(config.DB) + baseService := NewApprovalService(baseRepo) + baseHandler := NewApprovalHandler(baseService) + + adminGroup := api.Group("/needapprove") + adminGroup.Use(middleware.RequireAdminRole(), middleware.AuthMiddleware()) + + adminGroup.Get("/pending", baseHandler.GetPendingUsers) + + adminGroup.Get("/:user_id/approval-details", baseHandler.GetUserApprovalDetails) + adminGroup.Post("/approval-action", baseHandler.ProcessApprovalAction) + adminGroup.Post("/bulk-approval", baseHandler.BulkProcessApproval) + adminGroup.Post("/:user_id/approve", baseHandler.ApproveUser) + adminGroup.Post("/:user_id/reject", baseHandler.RejectUser) +} diff --git a/internal/admin/approval_service.go b/internal/admin/approval_service.go new file mode 100644 index 0000000..8db4057 --- /dev/null +++ b/internal/admin/approval_service.go @@ -0,0 +1,320 @@ +package admin + +import ( + "context" + "fmt" + "math" + "rijig/model" + "rijig/utils" + "strings" + "time" +) + +type ApprovalService interface { + GetPendingUsers(ctx context.Context, req *GetPendingUsersRequest) (*PendingUsersListResponse, error) + ApproveUser(ctx context.Context, userID, adminID string, notes string) (*ApprovalActionResponse, error) + RejectUser(ctx context.Context, userID, adminID string, notes string) (*ApprovalActionResponse, error) + ProcessApprovalAction(ctx context.Context, req *ApprovalActionRequest, adminID string) (*ApprovalActionResponse, error) + BulkProcessApproval(ctx context.Context, req *BulkApprovalRequest, adminID string) (*BulkApprovalResponse, error) + GetUserApprovalDetails(ctx context.Context, userID string) (*PendingUserResponse, error) +} + +type approvalService struct { + repo ApprovalRepository +} + +func NewApprovalService(repo ApprovalRepository) ApprovalService { + return &approvalService{ + repo: repo, + } +} + +func (s *approvalService) GetPendingUsers(ctx context.Context, req *GetPendingUsersRequest) (*PendingUsersListResponse, error) { + + req.SetDefaults() + + users, totalRecords, err := s.repo.GetPendingUsers(ctx, req) + if err != nil { + return nil, fmt.Errorf("failed to get pending users: %w", err) + } + + summary, err := s.repo.GetApprovalSummary(ctx) + if err != nil { + return nil, fmt.Errorf("failed to get approval summary: %w", err) + } + + totalPages := int(math.Ceil(float64(totalRecords) / float64(req.Limit))) + pagination := PaginationInfo{ + Page: req.Page, + Limit: req.Limit, + TotalPages: totalPages, + TotalRecords: totalRecords, + HasNext: req.Page < totalPages, + HasPrev: req.Page > 1, + } + + userResponses := make([]PendingUserResponse, len(users)) + for i, user := range users { + userResponses[i] = s.convertToUserResponse(user) + } + + return &PendingUsersListResponse{ + Users: userResponses, + Pagination: pagination, + Summary: *summary, + }, nil +} + +func (s *approvalService) ApproveUser(ctx context.Context, userID, adminID string, notes string) (*ApprovalActionResponse, error) { + + user, err := s.repo.GetUserByID(ctx, userID) + if err != nil { + return nil, fmt.Errorf("failed to get user: %w", err) + } + + if err := s.validateUserForApproval(user, "approved"); err != nil { + return nil, err + } + + previousStatus := user.RegistrationStatus + + newStatus := utils.RegStatusConfirmed + newProgress := utils.ProgressDataSubmitted + + if err := s.repo.UpdateUserRegistrationStatus(ctx, userID, newStatus, int8(newProgress)); err != nil { + return nil, fmt.Errorf("failed to approved user: %w", err) + } + + if err := s.revokeUserTokens(userID); err != nil { + + fmt.Printf("Warning: failed to revoke tokens for user %s: %v\n", userID, err) + } + + return &ApprovalActionResponse{ + UserID: userID, + Action: "approved", + PreviousStatus: previousStatus, + NewStatus: newStatus, + ProcessedAt: time.Now(), + ProcessedBy: adminID, + Notes: notes, + }, nil +} + +func (s *approvalService) RejectUser(ctx context.Context, userID, adminID string, notes string) (*ApprovalActionResponse, error) { + + user, err := s.repo.GetUserByID(ctx, userID) + if err != nil { + return nil, fmt.Errorf("failed to get user: %w", err) + } + + if err := s.validateUserForApproval(user, "rejected"); err != nil { + return nil, err + } + + previousStatus := user.RegistrationStatus + + newStatus := utils.RegStatusRejected + newProgress := utils.ProgressOTPVerified + + if err := s.repo.UpdateUserRegistrationStatus(ctx, userID, newStatus, int8(newProgress)); err != nil { + return nil, fmt.Errorf("failed to rejected user: %w", err) + } + + if err := s.revokeUserTokens(userID); err != nil { + + fmt.Printf("Warning: failed to revoke tokens for user %s: %v\n", userID, err) + } + + return &ApprovalActionResponse{ + UserID: userID, + Action: "rejected", + PreviousStatus: previousStatus, + NewStatus: newStatus, + ProcessedAt: time.Now(), + ProcessedBy: adminID, + Notes: notes, + }, nil +} + +func (s *approvalService) ProcessApprovalAction(ctx context.Context, req *ApprovalActionRequest, adminID string) (*ApprovalActionResponse, error) { + switch req.Action { + case "approved": + return s.ApproveUser(ctx, req.UserID, adminID, req.Notes) + case "rejected": + return s.RejectUser(ctx, req.UserID, adminID, req.Notes) + default: + return nil, fmt.Errorf("invalid action: %s", req.Action) + } +} + +func (s *approvalService) BulkProcessApproval(ctx context.Context, req *BulkApprovalRequest, adminID string) (*BulkApprovalResponse, error) { + + users, err := s.repo.GetUsersByIDs(ctx, req.UserIDs) + if err != nil { + return nil, fmt.Errorf("failed to get users: %w", err) + } + + var results []ApprovalActionResponse + var failures []ApprovalFailure + successCount := 0 + + for _, user := range users { + + if err := s.validateUserForApproval(&user, req.Action); err != nil { + failures = append(failures, ApprovalFailure{ + UserID: user.ID, + Error: "validation_failed", + Reason: err.Error(), + }) + continue + } + + actionReq := &ApprovalActionRequest{ + UserID: user.ID, + Action: req.Action, + Notes: req.Notes, + } + + result, err := s.ProcessApprovalAction(ctx, actionReq, adminID) + if err != nil { + failures = append(failures, ApprovalFailure{ + UserID: user.ID, + Error: "processing_failed", + Reason: err.Error(), + }) + continue + } + + results = append(results, *result) + successCount++ + } + + return &BulkApprovalResponse{ + SuccessCount: successCount, + FailureCount: len(failures), + Results: results, + Failures: failures, + }, nil +} + +func (s *approvalService) GetUserApprovalDetails(ctx context.Context, userID string) (*PendingUserResponse, error) { + user, err := s.repo.GetUserByID(ctx, userID) + if err != nil { + return nil, fmt.Errorf("failed to get user details: %w", err) + } + + userResponse := s.convertToUserResponse(*user) + return &userResponse, nil +} + +func (s *approvalService) validateUserForApproval(user *model.User, action string) error { + + if user.Role == nil { + return fmt.Errorf("user role not found") + } + + roleName := strings.ToLower(user.Role.RoleName) + if roleName != "pengelola" && roleName != "pengepul" { + return fmt.Errorf("only pengelola and pengepul can be approved/rejected") + } + + if user.RegistrationStatus != utils.RegStatusPending { + return fmt.Errorf("user is not in awaiting_approval status") + } + + if user.RegistrationProgress < utils.ProgressDataSubmitted { + return fmt.Errorf("user has not submitted required data yet") + } + + if roleName == "pengepul" && user.IdentityCard == nil { + return fmt.Errorf("pengepul must have identity card data") + } + + if roleName == "pengelola" && user.CompanyProfile == nil { + return fmt.Errorf("pengelola must have company profile data") + } + + return nil +} + +func (s *approvalService) revokeUserTokens(userID string) error { + + return utils.RevokeAllRefreshTokens(userID) +} + +func (s *approvalService) convertToUserResponse(user model.User) PendingUserResponse { + response := PendingUserResponse{ + ID: user.ID, + Name: user.Name, + Phone: user.Phone, + Email: user.Email, + RegistrationStatus: user.RegistrationStatus, + RegistrationProgress: user.RegistrationProgress, + SubmittedAt: user.UpdatedAt, + } + + if user.Role != nil { + response.Role = RoleInfo{ + ID: user.Role.ID, + RoleName: user.Role.RoleName, + } + } + + stepInfo := utils.GetRegistrationStepInfo( + user.Role.RoleName, + int(user.RegistrationProgress), + user.RegistrationStatus, + ) + if stepInfo != nil { + response.RegistrationStepInfo = &RegistrationStepResponse{ + Step: stepInfo.Step, + Status: stepInfo.Status, + Description: stepInfo.Description, + RequiresAdminApproval: stepInfo.RequiresAdminApproval, + IsAccessible: stepInfo.IsAccessible, + IsCompleted: stepInfo.IsCompleted, + } + } + + if user.IdentityCard != nil { + response.IdentityCard = &IdentityCardInfo{ + ID: user.IdentityCard.ID, + IdentificationNumber: user.IdentityCard.Identificationumber, + Fullname: user.IdentityCard.Fullname, + Placeofbirth: user.IdentityCard.Placeofbirth, + Dateofbirth: user.IdentityCard.Dateofbirth, + Gender: user.IdentityCard.Gender, + BloodType: user.IdentityCard.BloodType, + Province: user.IdentityCard.Province, + District: user.IdentityCard.District, + SubDistrict: user.IdentityCard.SubDistrict, + Village: user.IdentityCard.Village, + PostalCode: user.IdentityCard.PostalCode, + Religion: user.IdentityCard.Religion, + Maritalstatus: user.IdentityCard.Maritalstatus, + Job: user.IdentityCard.Job, + Citizenship: user.IdentityCard.Citizenship, + Validuntil: user.IdentityCard.Validuntil, + Cardphoto: user.IdentityCard.Cardphoto, + } + } + + if user.CompanyProfile != nil { + response.CompanyProfile = &CompanyProfileInfo{ + ID: user.CompanyProfile.ID, + CompanyName: user.CompanyProfile.CompanyName, + CompanyAddress: user.CompanyProfile.CompanyAddress, + CompanyPhone: user.CompanyProfile.CompanyPhone, + CompanyEmail: user.CompanyProfile.CompanyEmail, + CompanyLogo: user.CompanyProfile.CompanyLogo, + CompanyWebsite: user.CompanyProfile.CompanyWebsite, + TaxID: user.CompanyProfile.TaxID, + FoundedDate: user.CompanyProfile.FoundedDate, + CompanyType: user.CompanyProfile.CompanyType, + CompanyDescription: user.CompanyProfile.CompanyDescription, + } + } + + return response +} diff --git a/internal/authentication/authentication_service.go b/internal/authentication/authentication_service.go index 980c5c8..e33cd62 100644 --- a/internal/authentication/authentication_service.go +++ b/internal/authentication/authentication_service.go @@ -147,7 +147,7 @@ func (s *authenticationService) LoginAdmin(ctx context.Context, req *LoginAdminR return nil, fmt.Errorf("invalid credentials") } - if user.RegistrationStatus != "completed" { + if user.RegistrationStatus != "complete" { return nil, fmt.Errorf("account not activated") } @@ -470,7 +470,7 @@ func (s *authenticationService) RegisterAdmin(ctx context.Context, req *Register Password: hashedPassword, RoleID: role.ID, RegistrationStatus: "pending_email_verification", - RegistrationProgress: 1, + // RegistrationProgress: 1, EmailVerified: false, } diff --git a/internal/chat/model/chat_model.go b/internal/chat/model/chat_model.go deleted file mode 100644 index 0c3b4f2..0000000 --- a/internal/chat/model/chat_model.go +++ /dev/null @@ -1 +0,0 @@ -package model \ No newline at end of file diff --git a/internal/userprofile/userprofile_service.go b/internal/userprofile/userprofile_service.go index dd405d1..0946ec2 100644 --- a/internal/userprofile/userprofile_service.go +++ b/internal/userprofile/userprofile_service.go @@ -105,8 +105,6 @@ func (s *userProfileService) UpdateRegistUserProfile(ctx context.Context, userID NextStep: nextStep, SessionID: tokenResponse.SessionID, }, nil - - // return s.mapToResponseDTO(updatedUser), nil } func (s *userProfileService) mapToResponseDTO(user *model.User) *UserProfileResponseDTO { diff --git a/model/user_model.go b/model/user_model.go index 323c6ac..c4724b9 100644 --- a/model/user_model.go +++ b/model/user_model.go @@ -3,21 +3,23 @@ package model import "time" type User struct { - ID string `gorm:"primaryKey;type:uuid;default:uuid_generate_v4();unique;not null" json:"id"` - Avatar *string `json:"avatar,omitempty"` - Name string `gorm:"not null" json:"name"` - Gender string `gorm:"not null" json:"gender"` - Dateofbirth string `gorm:"not null" json:"dateofbirth"` - Placeofbirth string `gorm:"not null" json:"placeofbirth"` - Phone string `gorm:"not null;index" json:"phone"` - Email string `json:"email,omitempty"` - EmailVerified bool `gorm:"default:false" json:"emailVerified"` - PhoneVerified bool `gorm:"default:false" json:"phoneVerified"` - Password string `json:"password,omitempty"` - RoleID string `gorm:"not null" json:"roleId"` - Role *Role `gorm:"foreignKey:RoleID;constraint:OnUpdate:CASCADE,OnDelete:SET NULL;" json:"role"` - RegistrationStatus string `json:"registrationstatus"` - RegistrationProgress int8 `json:"registration_progress"` - CreatedAt time.Time `gorm:"default:current_timestamp" json:"createdAt"` - UpdatedAt time.Time `gorm:"default:current_timestamp" json:"updatedAt"` + ID string `gorm:"primaryKey;type:uuid;default:uuid_generate_v4();unique;not null" json:"id"` + Avatar *string `json:"avatar,omitempty"` + Name string `gorm:"not null" json:"name"` + Gender string `gorm:"not null" json:"gender"` + Dateofbirth string `gorm:"not null" json:"dateofbirth"` + Placeofbirth string `gorm:"not null" json:"placeofbirth"` + Phone string `gorm:"not null;index" json:"phone"` + Email string `json:"email,omitempty"` + EmailVerified bool `gorm:"default:false" json:"emailVerified"` + PhoneVerified bool `gorm:"default:false" json:"phoneVerified"` + Password string `json:"password,omitempty"` + RoleID string `gorm:"not null" json:"roleId"` + Role *Role `gorm:"foreignKey:RoleID;constraint:OnUpdate:CASCADE,OnDelete:SET NULL;" json:"role"` + RegistrationStatus string `json:"registrationstatus"` + RegistrationProgress int8 `json:"registration_progress"` + IdentityCard *IdentityCard `gorm:"foreignKey:UserID;constraint:OnUpdate:CASCADE,OnDelete:CASCADE;" json:"identity_card,omitempty"` + CompanyProfile *CompanyProfile `gorm:"foreignKey:UserID;constraint:OnUpdate:CASCADE,OnDelete:CASCADE;" json:"company_profile,omitempty"` + CreatedAt time.Time `gorm:"default:current_timestamp" json:"createdAt"` + UpdatedAt time.Time `gorm:"default:current_timestamp" json:"updatedAt"` } diff --git a/router/setup_routes.go.go b/router/setup_routes.go.go index ea43573..0fbcdba 100644 --- a/router/setup_routes.go.go +++ b/router/setup_routes.go.go @@ -4,6 +4,7 @@ import ( "os" "rijig/internal/about" + "rijig/internal/admin" "rijig/internal/article" "rijig/internal/authentication" "rijig/internal/company" @@ -42,6 +43,7 @@ func SetupRoutes(app *fiber.App) { trash.TrashRouter(api) about.AboutRouter(api) whatsapp.WhatsAppRouter(api) + admin.ApprovalRoutes(api) // || auth router || // // presentation.AuthRouter(api)