package service import ( "errors" "fmt" "netidhub-saas-be/app/database/entity" "netidhub-saas-be/app/module/clients/mapper" "netidhub-saas-be/app/module/clients/repository" "netidhub-saas-be/app/module/clients/request" "netidhub-saas-be/app/module/clients/response" usersRepository "netidhub-saas-be/app/module/users/repository" usersRequest "netidhub-saas-be/app/module/users/request" usersService "netidhub-saas-be/app/module/users/service" "netidhub-saas-be/utils/paginator" "strings" "time" "github.com/gofiber/fiber/v2" "github.com/google/uuid" "github.com/rs/zerolog" config "netidhub-saas-be/config/config" utilSvc "netidhub-saas-be/utils/service" ) // ClientsService type clientsService struct { Repo repository.ClientsRepository Cfg *config.Config UsersRepo usersRepository.UsersRepository UsersSvc usersService.UsersService ClientLogoUploadSvc *ClientLogoUploadService Log zerolog.Logger } // ClientsService define interface of IClientsService type ClientsService interface { All(authToken string, req request.ClientsQueryRequest) (clients []*response.ClientsResponse, paging paginator.Pagination, err error) PublicAll(req request.ClientsQueryRequest) (clients []*response.PublicClientsResponse, paging paginator.Pagination, err error) Show(id uuid.UUID) (clients *response.ClientsResponse, err error) CheckClientNameExists(name string) (exists bool, err error) ShowWithAuth(authToken string) (clients *response.ClientsResponse, err error) ShowBySlugPublic(slug string) (clients *response.PublicClientsResponse, err error) Save(req request.ClientsCreateRequest, authToken string) (clients *entity.Clients, err error) Update(id uuid.UUID, req request.ClientsUpdateRequest) (err error) UpdateWithAuth(authToken string, req request.ClientsUpdateRequest) (err error) Delete(id uuid.UUID) error // New hierarchy methods CreateSubClient(parentId uuid.UUID, req request.ClientsCreateRequest) (*entity.Clients, error) MoveClient(clientId uuid.UUID, req request.MoveClientRequest) error GetHierarchy(clientId uuid.UUID) (*response.ClientHierarchyResponse, error) GetClientStats(clientId uuid.UUID) (*response.ClientStatsResponse, error) BulkCreateSubClients(req request.BulkCreateSubClientsRequest) (*response.BulkOperationResponse, error) // Client with user creation CreateClientWithUser(req request.ClientWithUserCreateRequest) (*response.ClientWithUserResponse, error) // Logo upload methods UploadLogo(authToken string, c *fiber.Ctx) (string, error) DeleteLogo(clientId uuid.UUID, imagePath string) error GetLogoURL(imagePath string) (string, error) ViewLogo(filename string) ([]byte, string, error) } // NewClientsService init ClientsService func NewClientsService(repo repository.ClientsRepository, cfg *config.Config, log zerolog.Logger, usersRepo usersRepository.UsersRepository, usersSvc usersService.UsersService, clientLogoUploadSvc *ClientLogoUploadService) ClientsService { return &clientsService{ Repo: repo, Cfg: cfg, Log: log, UsersRepo: usersRepo, UsersSvc: usersSvc, ClientLogoUploadSvc: clientLogoUploadSvc, } } // All implement interface of ClientsService func (_i *clientsService) All(authToken string, req request.ClientsQueryRequest) (clientss []*response.ClientsResponse, paging paginator.Pagination, err error) { // Extract clientId from authToken var clientId *uuid.UUID if authToken != "" { user := utilSvc.GetUserInfo(_i.Log, _i.UsersRepo, authToken) if user != nil && user.ClientId != nil { clientId = user.ClientId _i.Log.Info().Interface("clientId", clientId).Msg("Extracted clientId from auth token") } } results, paging, err := _i.Repo.GetAll(req) if err != nil { return } for _, result := range results { clientss = append(clientss, mapper.ClientsResponseMapper(result)) } return } func (_i *clientsService) PublicAll(req request.ClientsQueryRequest) (clientss []*response.PublicClientsResponse, paging paginator.Pagination, err error) { _i.Log.Info().Interface("data", req).Msg("Getting public clients list") // Only return active clients for public consumption isActive := true req.IsActive = &isActive results, paging, err := _i.Repo.GetAll(req) if err != nil { return } for _, result := range results { clientss = append(clientss, mapper.PublicClientsResponseMapper(result)) } _i.Log.Info().Int("count", len(clientss)).Msg("Public clients retrieved successfully") return } func (_i *clientsService) Show(id uuid.UUID) (clients *response.ClientsResponse, err error) { result, err := _i.Repo.FindOne(id) if err != nil { return nil, err } return mapper.ClientsResponseMapper(result), nil } func (_i *clientsService) CheckClientNameExists(name string) (exists bool, err error) { _i.Log.Info().Str("name", name).Msg("Checking if client name exists") // Check if client name exists in repository result, err := _i.Repo.FindByName(name) if err != nil { // If error is "record not found", name doesn't exist if strings.Contains(err.Error(), "record not found") { return false, nil } _i.Log.Error().Err(err).Str("name", name).Msg("Failed to check client name existence") return false, err } // If result is not nil, name exists if result != nil { return true, nil } return false, nil } func (_i *clientsService) ShowWithAuth(authToken string) (clients *response.ClientsResponse, err error) { // Extract clientId from authToken user := utilSvc.GetUserInfo(_i.Log, _i.UsersRepo, authToken) if user == nil { _i.Log.Error().Msg("User not found from auth token") return nil, fmt.Errorf("user not found") } if user.ClientId == nil { _i.Log.Error().Msg("Client ID not found in user token") return nil, fmt.Errorf("client ID not found in user token") } clientId := *user.ClientId _i.Log.Info().Str("clientId", clientId.String()).Msg("Getting client details with auth token") result, err := _i.Repo.FindOne(clientId) if err != nil { _i.Log.Error().Err(err).Str("clientId", clientId.String()).Msg("Failed to find client") return nil, err } return mapper.ClientsResponseMapper(result), nil } func (_i *clientsService) ShowBySlugPublic(slug string) (clients *response.PublicClientsResponse, err error) { _i.Log.Info().Str("slug", slug).Msg("Getting client by slug for public consumption") // For public access, only return active clients result, err := _i.Repo.FindBySlugPublic(slug) if err != nil { _i.Log.Error().Err(err).Str("slug", slug).Msg("Failed to find client by slug") return nil, err } return mapper.PublicClientsResponseMapper(result), nil } func (_i *clientsService) Save(req request.ClientsCreateRequest, authToken string) (clients *entity.Clients, err error) { _i.Log.Info().Interface("data", req).Msg("") // Generate slug from client name slug := utilSvc.MakeSlug(req.Name) // Convert request to entity newReq := &entity.Clients{ Name: req.Name, Slug: slug, Description: req.Description, ClientType: req.ClientType, ParentClientId: req.ParentClientId, MaxUsers: req.MaxUsers, MaxStorage: req.MaxStorage, Settings: req.Settings, } _i.Log.Info().Interface("token", authToken).Msg("") createdBy := utilSvc.GetUserInfo(_i.Log, _i.UsersRepo, authToken) _i.Log.Info().Interface("token", authToken).Msg("") newReq.CreatedById = &createdBy.ID newReq.ID = uuid.New() _i.Log.Info().Interface("new data", newReq).Msg("") return _i.Repo.Create(newReq) } func (_i *clientsService) Update(id uuid.UUID, req request.ClientsUpdateRequest) (err error) { _i.Log.Info().Interface("data", req).Msg("") // Convert request to entity updateReq := &entity.Clients{ Name: *req.Name, Description: req.Description, ClientType: *req.ClientType, ParentClientId: req.ParentClientId, LogoUrl: req.LogoUrl, LogoImagePath: req.LogoImagePath, Address: req.Address, PhoneNumber: req.PhoneNumber, Website: req.Website, MaxUsers: req.MaxUsers, MaxStorage: req.MaxStorage, Settings: req.Settings, IsActive: req.IsActive, } return _i.Repo.Update(id, updateReq) } func (_i *clientsService) UpdateWithAuth(authToken string, req request.ClientsUpdateRequest) (err error) { _i.Log.Info().Interface("data", req).Msg("") // Extract clientId from authToken user := utilSvc.GetUserInfo(_i.Log, _i.UsersRepo, authToken) if user == nil { _i.Log.Error().Msg("User not found from auth token") return fmt.Errorf("user not found") } if user.ClientId == nil { _i.Log.Error().Msg("Client ID not found in user token") return fmt.Errorf("client ID not found in user token") } clientId := *user.ClientId _i.Log.Info().Str("clientId", clientId.String()).Msg("Updating client with auth token") // Convert request to entity updateReq := &entity.Clients{ Name: *req.Name, Description: req.Description, ParentClientId: req.ParentClientId, LogoUrl: req.LogoUrl, LogoImagePath: req.LogoImagePath, Address: req.Address, PhoneNumber: req.PhoneNumber, Website: req.Website, MaxUsers: req.MaxUsers, MaxStorage: req.MaxStorage, Settings: req.Settings, IsActive: req.IsActive, } _i.Log.Info().Interface("Updating client with auth token", updateReq).Msg("") return _i.Repo.Update(clientId, updateReq) } func (_i *clientsService) Delete(id uuid.UUID) error { result, err := _i.Repo.FindOne(id) if err != nil { return err } isActive := false result.IsActive = &isActive return _i.Repo.Update(id, result) } // ===================================================================== // NEW HIERARCHY METHODS // ===================================================================== // CreateSubClient creates a client under a parent func (_i *clientsService) CreateSubClient(parentId uuid.UUID, req request.ClientsCreateRequest) (*entity.Clients, error) { // Validate parent exists _, err := _i.Repo.FindOne(parentId) if err != nil { return nil, errors.New("parent client not found") } // Set client type and parent req.ClientType = "sub_client" req.ParentClientId = &parentId // Generate slug from client name slug := utilSvc.MakeSlug(req.Name) // Convert to entity newReq := &entity.Clients{ Name: req.Name, Slug: slug, Description: req.Description, ClientType: req.ClientType, ParentClientId: req.ParentClientId, MaxUsers: req.MaxUsers, MaxStorage: req.MaxStorage, Settings: req.Settings, } newReq.ID = uuid.New() return _i.Repo.Create(newReq) } // MoveClient moves a client to different parent func (_i *clientsService) MoveClient(clientId uuid.UUID, req request.MoveClientRequest) error { client, err := _i.Repo.FindOne(clientId) if err != nil { return errors.New("client not found") } // If moving to root (standalone) if req.TargetParentId == nil { client.ClientType = "standalone" client.ParentClientId = nil return _i.Repo.Update(clientId, client) } // Validate target parent exists _, err = _i.Repo.FindOne(*req.TargetParentId) if err != nil { return errors.New("target parent not found") } // Move return _i.Repo.MoveClient(clientId, *req.TargetParentId) } // GetHierarchy gets full client tree func (_i *clientsService) GetHierarchy(clientId uuid.UUID) (*response.ClientHierarchyResponse, error) { client, err := _i.Repo.GetWithHierarchy(clientId) if err != nil { return nil, err } return _i.buildHierarchyResponse(client, 0, []string{}), nil } // buildHierarchyResponse recursively builds hierarchy func (_i *clientsService) buildHierarchyResponse(client *entity.Clients, level int, path []string) *response.ClientHierarchyResponse { currentPath := append(path, client.Name) resp := &response.ClientHierarchyResponse{ ID: client.ID, Name: client.Name, Slug: client.Slug, Description: client.Description, ClientType: client.ClientType, Level: level, Path: currentPath, ParentClientId: client.ParentClientId, LogoUrl: client.LogoUrl, LogoImagePath: client.LogoImagePath, Address: client.Address, PhoneNumber: client.PhoneNumber, Website: client.Website, IsActive: client.IsActive, } // Count users (simplified - would need proper DB access) resp.CurrentUsers = 0 // TODO: implement user count // Build sub-clients recursively if client.SubClients != nil { for _, subClient := range client.SubClients { resp.SubClients = append(resp.SubClients, *_i.buildHierarchyResponse(&subClient, level+1, currentPath)) } } return resp } // GetClientStats gets comprehensive statistics func (_i *clientsService) GetClientStats(clientId uuid.UUID) (*response.ClientStatsResponse, error) { client, err := _i.Repo.FindOne(clientId) if err != nil { return nil, err } stats, err := _i.Repo.GetClientStats(clientId) if err != nil { return nil, err } isParent, _ := _i.Repo.IsParentClient(clientId) return &response.ClientStatsResponse{ ClientId: client.ID, ClientName: client.Name, TotalUsers: stats["total_users"].(int), TotalArticles: stats["total_articles"].(int), SubClientCount: stats["sub_client_count"].(int), IsParent: isParent, }, nil } // BulkCreateSubClients creates multiple sub-clients func (_i *clientsService) BulkCreateSubClients(req request.BulkCreateSubClientsRequest) (*response.BulkOperationResponse, error) { results := []response.BulkOperationResult{} successful := 0 failed := 0 for i, subClientReq := range req.SubClients { createReq := request.ClientsCreateRequest{ Name: subClientReq.Name, Description: subClientReq.Description, ClientType: "sub_client", ParentClientId: &req.ParentClientId, MaxUsers: subClientReq.MaxUsers, MaxStorage: subClientReq.MaxStorage, } client, err := _i.CreateSubClient(req.ParentClientId, createReq) if err != nil { failed++ errMsg := err.Error() results = append(results, response.BulkOperationResult{ Index: i, Name: subClientReq.Name, Success: false, Error: &errMsg, }) } else { successful++ results = append(results, response.BulkOperationResult{ Index: i, ClientId: &client.ID, Name: client.Name, Success: true, }) } } return &response.BulkOperationResponse{ TotalRequested: len(req.SubClients), Successful: successful, Failed: failed, Results: results, }, nil } // CreateClientWithUser creates a client and admin user in one transaction func (_i *clientsService) CreateClientWithUser(req request.ClientWithUserCreateRequest) (*response.ClientWithUserResponse, error) { _i.Log.Info().Interface("data", req).Msg("Creating client with admin user (Public endpoint)") // Step 1: Create the client clientReq := req.Client // Generate slug from client name slug := utilSvc.MakeSlug(clientReq.Name) newClient := &entity.Clients{ Name: clientReq.Name, Slug: slug, Description: clientReq.Description, ClientType: clientReq.ClientType, ParentClientId: clientReq.ParentClientId, MaxUsers: clientReq.MaxUsers, MaxStorage: clientReq.MaxStorage, Settings: clientReq.Settings, } // Generate new UUID for client newClient.ID = uuid.New() // For public endpoint, no created by user newClient.CreatedById = nil // Create client createdClient, err := _i.Repo.Create(newClient) if err != nil { _i.Log.Error().Err(err).Msg("Failed to create client") return nil, fmt.Errorf("failed to create client: %w", err) } _i.Log.Info().Interface("clientId", createdClient.ID).Msg("Client created successfully") // Step 2: Create admin user for the client adminUserReq := req.AdminUser // Convert to UsersCreateRequest format userCreateReq := usersRequest.UsersCreateRequest{ Username: adminUserReq.Username, Email: adminUserReq.Email, Fullname: adminUserReq.Fullname, Password: adminUserReq.Password, PhoneNumber: adminUserReq.PhoneNumber, Address: adminUserReq.Address, WorkType: adminUserReq.WorkType, GenderType: adminUserReq.GenderType, IdentityType: adminUserReq.IdentityType, IdentityGroup: adminUserReq.IdentityGroup, IdentityGroupNumber: adminUserReq.IdentityGroupNumber, IdentityNumber: adminUserReq.IdentityNumber, DateOfBirth: adminUserReq.DateOfBirth, LastEducation: adminUserReq.LastEducation, ClientId: &createdClient.ID, // Set default admin level and role (you may need to adjust these based on your system) UserLevelId: 1, // Assuming level 1 is generic level UserRoleId: 2, // Assuming role 1 is admin client role } // Create user with the new client ID createdUser, err := _i.UsersSvc.Save("", userCreateReq) if err != nil { _i.Log.Error().Err(err).Msg("Failed to create admin user") // Rollback: delete the created client _i.Repo.Delete(createdClient.ID) return nil, fmt.Errorf("failed to create admin user: %w", err) } _i.Log.Info().Interface("userId", createdUser.ID).Msg("Admin user created successfully") _i.Log.Info().Interface("createdClient", createdClient).Msg("Created Client") // Step 3: Prepare response clientResponse := mapper.ClientsResponseMapper(createdClient) adminUserResponse := response.AdminUserResponse{ ID: createdUser.ID, Username: createdUser.Username, Email: createdUser.Email, Fullname: createdUser.Fullname, UserLevelId: createdUser.UserLevelId, UserRoleId: createdUser.UserRoleId, PhoneNumber: createdUser.PhoneNumber, Address: createdUser.Address, WorkType: createdUser.WorkType, GenderType: createdUser.GenderType, IdentityType: createdUser.IdentityType, IdentityGroup: createdUser.IdentityGroup, IdentityGroupNumber: createdUser.IdentityGroupNumber, IdentityNumber: createdUser.IdentityNumber, DateOfBirth: createdUser.DateOfBirth, LastEducation: createdUser.LastEducation, KeycloakId: createdUser.KeycloakId, ClientId: *createdUser.ClientId, IsActive: *createdUser.IsActive, CreatedAt: createdUser.CreatedAt, UpdatedAt: createdUser.UpdatedAt, } return &response.ClientWithUserResponse{ Client: *clientResponse, AdminUser: adminUserResponse, Message: fmt.Sprintf("Client '%s' and admin user '%s' created successfully", createdClient.Name, createdUser.Username), }, nil } // ===================================================================== // LOGO UPLOAD METHODS // ===================================================================== // UploadLogo uploads client logo to MinIO func (_i *clientsService) UploadLogo(authToken string, c *fiber.Ctx) (string, error) { // Extract clientId from authToken user := utilSvc.GetUserInfo(_i.Log, _i.UsersRepo, authToken) if user == nil { _i.Log.Error().Msg("User not found from auth token") return "", fmt.Errorf("user not found") } if user.ClientId == nil { _i.Log.Error().Msg("Client ID not found in user token") return "", fmt.Errorf("client ID not found in user token") } clientId := *user.ClientId _i.Log.Info().Str("clientId", clientId.String()).Msg("Uploading client logo") // Fetch client data to get client name client, err := _i.Repo.FindOne(clientId) if err != nil { _i.Log.Error().Err(err).Str("clientId", clientId.String()).Msg("Failed to fetch client data") return "", fmt.Errorf("failed to fetch client data: %w", err) } // Upload logo using the upload service imagePath, newFilename, err := _i.ClientLogoUploadSvc.UploadLogo(c, clientId.String(), client.Slug) if err != nil { _i.Log.Error().Err(err).Str("clientId", clientId.String()).Msg("Failed to upload client logo") return "", err } _i.Log.Info(). Str("Upload imagePath", imagePath).Msg("Client upload logo files") logoUrl := fmt.Sprintf("%s/%s/%s", _i.Cfg.App.Domain, "clients/logo", newFilename) // Update client with new logo image path updateReq := request.ClientsUpdateRequest{ LogoImagePath: &imagePath, LogoUrl: &logoUrl, } _i.Log.Info(). Str("Upload Update Request", imagePath).Interface("updateReq ", updateReq).Msg("Client upload logo files") err = _i.Repo.Update(clientId, updateReq.ToEntity()) if err != nil { _i.Log.Error().Err(err).Str("clientId", clientId.String()).Str("imagePath", imagePath).Msg("Failed to update client with logo path") // Try to clean up uploaded file cleanupErr := _i.ClientLogoUploadSvc.DeleteLogo(clientId.String(), imagePath) if cleanupErr != nil { _i.Log.Error().Err(cleanupErr).Str("imagePath", imagePath).Msg("Failed to cleanup uploaded logo after update failure") } return "", fmt.Errorf("failed to update client with logo path: %w", err) } _i.Log.Info().Str("clientId", clientId.String()).Str("imagePath", imagePath).Msg("Client logo uploaded and updated successfully") return imagePath, nil } // DeleteLogo deletes client logo from MinIO func (_i *clientsService) DeleteLogo(clientId uuid.UUID, imagePath string) error { _i.Log.Info().Str("clientId", clientId.String()).Str("imagePath", imagePath).Msg("Deleting client logo") err := _i.ClientLogoUploadSvc.DeleteLogo(clientId.String(), imagePath) if err != nil { _i.Log.Error().Err(err).Str("clientId", clientId.String()).Str("imagePath", imagePath).Msg("Failed to delete client logo") return err } // Clear logo image path from client emptyPath := "" updateReq := request.ClientsUpdateRequest{ LogoImagePath: &emptyPath, } err = _i.Repo.Update(clientId, updateReq.ToEntity()) if err != nil { _i.Log.Error().Err(err).Str("clientId", clientId.String()).Msg("Failed to clear logo path from client") return fmt.Errorf("failed to clear logo path from client: %w", err) } _i.Log.Info().Str("clientId", clientId.String()).Msg("Client logo deleted successfully") return nil } // GetLogoURL generates a presigned URL for the logo func (_i *clientsService) GetLogoURL(imagePath string) (string, error) { if imagePath == "" { return "", nil } _i.Log.Info().Str("imagePath", imagePath).Msg("Generating logo URL") // Generate presigned URL valid for 24 hours url, err := _i.ClientLogoUploadSvc.GetLogoURL(imagePath, 24*time.Hour) if err != nil { _i.Log.Error().Err(err).Str("imagePath", imagePath).Msg("Failed to generate logo URL") return "", err } _i.Log.Info().Str("imagePath", imagePath).Str("url", url).Msg("Logo URL generated successfully") return url, nil } // ViewLogo retrieves logo file from MinIO func (_i *clientsService) ViewLogo(filename string) ([]byte, string, error) { _i.Log.Info().Str("filename", filename).Msg("Retrieving logo file") client, err := _i.Repo.FindByImagePathName(filename) if err != nil { _i.Log.Error().Err(err).Str("filename", filename).Msg("Failed to find client by image path name") return nil, "", err } data, contentType, err := _i.ClientLogoUploadSvc.GetLogoFile(client.ID.String(), filename) if err != nil { _i.Log.Error().Err(err).Str("filename", filename).Msg("Failed to retrieve logo file") return nil, "", err } _i.Log.Info().Str("filename", filename).Str("contentType", contentType).Int("dataSize", len(data)).Msg("Logo file retrieved successfully") return data, contentType, nil }