Compare commits

..

1 Commits
main ... api_v3

Author SHA1 Message Date
pahmiudahgede 094a147489 feat: admin view approval needed 2025-07-09 14:14:42 +07:00
10 changed files with 988 additions and 22 deletions

View File

@ -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"
}
}

View File

@ -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)
}

View File

@ -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
}

View File

@ -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)
}

View File

@ -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
}

View File

@ -147,7 +147,7 @@ func (s *authenticationService) LoginAdmin(ctx context.Context, req *LoginAdminR
return nil, fmt.Errorf("invalid credentials") return nil, fmt.Errorf("invalid credentials")
} }
if user.RegistrationStatus != "completed" { if user.RegistrationStatus != "complete" {
return nil, fmt.Errorf("account not activated") return nil, fmt.Errorf("account not activated")
} }
@ -470,7 +470,7 @@ func (s *authenticationService) RegisterAdmin(ctx context.Context, req *Register
Password: hashedPassword, Password: hashedPassword,
RoleID: role.ID, RoleID: role.ID,
RegistrationStatus: "pending_email_verification", RegistrationStatus: "pending_email_verification",
RegistrationProgress: 1, // RegistrationProgress: 1,
EmailVerified: false, EmailVerified: false,
} }

View File

@ -1 +0,0 @@
package model

View File

@ -105,8 +105,6 @@ func (s *userProfileService) UpdateRegistUserProfile(ctx context.Context, userID
NextStep: nextStep, NextStep: nextStep,
SessionID: tokenResponse.SessionID, SessionID: tokenResponse.SessionID,
}, nil }, nil
// return s.mapToResponseDTO(updatedUser), nil
} }
func (s *userProfileService) mapToResponseDTO(user *model.User) *UserProfileResponseDTO { func (s *userProfileService) mapToResponseDTO(user *model.User) *UserProfileResponseDTO {

View File

@ -3,21 +3,23 @@ package model
import "time" import "time"
type User struct { type User struct {
ID string `gorm:"primaryKey;type:uuid;default:uuid_generate_v4();unique;not null" json:"id"` ID string `gorm:"primaryKey;type:uuid;default:uuid_generate_v4();unique;not null" json:"id"`
Avatar *string `json:"avatar,omitempty"` Avatar *string `json:"avatar,omitempty"`
Name string `gorm:"not null" json:"name"` Name string `gorm:"not null" json:"name"`
Gender string `gorm:"not null" json:"gender"` Gender string `gorm:"not null" json:"gender"`
Dateofbirth string `gorm:"not null" json:"dateofbirth"` Dateofbirth string `gorm:"not null" json:"dateofbirth"`
Placeofbirth string `gorm:"not null" json:"placeofbirth"` Placeofbirth string `gorm:"not null" json:"placeofbirth"`
Phone string `gorm:"not null;index" json:"phone"` Phone string `gorm:"not null;index" json:"phone"`
Email string `json:"email,omitempty"` Email string `json:"email,omitempty"`
EmailVerified bool `gorm:"default:false" json:"emailVerified"` EmailVerified bool `gorm:"default:false" json:"emailVerified"`
PhoneVerified bool `gorm:"default:false" json:"phoneVerified"` PhoneVerified bool `gorm:"default:false" json:"phoneVerified"`
Password string `json:"password,omitempty"` Password string `json:"password,omitempty"`
RoleID string `gorm:"not null" json:"roleId"` RoleID string `gorm:"not null" json:"roleId"`
Role *Role `gorm:"foreignKey:RoleID;constraint:OnUpdate:CASCADE,OnDelete:SET NULL;" json:"role"` Role *Role `gorm:"foreignKey:RoleID;constraint:OnUpdate:CASCADE,OnDelete:SET NULL;" json:"role"`
RegistrationStatus string `json:"registrationstatus"` RegistrationStatus string `json:"registrationstatus"`
RegistrationProgress int8 `json:"registration_progress"` RegistrationProgress int8 `json:"registration_progress"`
CreatedAt time.Time `gorm:"default:current_timestamp" json:"createdAt"` IdentityCard *IdentityCard `gorm:"foreignKey:UserID;constraint:OnUpdate:CASCADE,OnDelete:CASCADE;" json:"identity_card,omitempty"`
UpdatedAt time.Time `gorm:"default:current_timestamp" json:"updatedAt"` 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"`
} }

View File

@ -4,6 +4,7 @@ import (
"os" "os"
"rijig/internal/about" "rijig/internal/about"
"rijig/internal/admin"
"rijig/internal/article" "rijig/internal/article"
"rijig/internal/authentication" "rijig/internal/authentication"
"rijig/internal/company" "rijig/internal/company"
@ -42,6 +43,7 @@ func SetupRoutes(app *fiber.App) {
trash.TrashRouter(api) trash.TrashRouter(api)
about.AboutRouter(api) about.AboutRouter(api)
whatsapp.WhatsAppRouter(api) whatsapp.WhatsAppRouter(api)
admin.ApprovalRoutes(api)
// || auth router || // // || auth router || //
// presentation.AuthRouter(api) // presentation.AuthRouter(api)