package usecase import ( "errors" "fmt" "mime/multipart" "strings" "time" "users_management/m/model/dto/req" "users_management/m/model/dto/res" "users_management/m/model/entity" "users_management/m/repository" "users_management/m/utils/helper" "users_management/m/utils/service" "github.com/go-playground/validator/v10" "github.com/google/uuid" ) type DeviceDetailsUseCase interface { CreateDeviceDetails(device req.DeviceDetailsDTO) error GetAllDeviceDetails() ([]res.DeviceDetailsResponse, error) GetDeviceDetailsByID(id uuid.UUID) (res.DeviceDetailsResponse, error) UpdateDeviceDetails(id uuid.UUID, device req.UpdateDeviceDetailsDTO) error DeleteDeviceDetails(id uuid.UUID) error // Port management // ValidatePortUsage(deviceID uuid.UUID, requiredPorts int) error RecalculatePortUsage(deviceID uuid.UUID) error AssignCustomerToPort(deviceID uuid.UUID, customerName string,portNumber *int) error UpdatePortUsage(deviceID uuid.UUID, portUsed int) error UpdatePortAssignments(deviceID uuid.UUID, portAssignments []req.PortAssignmentDTO) error AssignMultipleCustomersToPort(deviceID uuid.UUID, assignments []req.AssignMultipleCustomersDTO) error UpdateCustomerByPort(deviceID uuid.UUID, update req.UpdateCustomerByPortDTO) error BulkUpdateCustomersByPort(deviceID uuid.UUID, updates []req.UpdateCustomerByPortDTO) error RemoveCustomerByPort(deviceID uuid.UUID, portNumber int) error UpdateDeviceDetailsWithMultipleImages(id uuid.UUID, deviceDTO req.UpdateDeviceDetailsDTO, imageFiles []*multipart.FileHeader, replaceImages ...bool) error DeleteDeviceImage(deviceID uuid.UUID, filename string) error } type deviceDetailsUseCase struct { deviceDetailsRepo repository.DeviceDetailsRepo geocoder service.GeocodingService validate *validator.Validate } func NewDeviceDetailsUseCase(deviceDetailsRepo repository.DeviceDetailsRepo, geocoder service.GeocodingService) DeviceDetailsUseCase { return &deviceDetailsUseCase{ deviceDetailsRepo: deviceDetailsRepo, geocoder: geocoder, validate: validator.New(), } } func (u *deviceDetailsUseCase) DeleteDeviceImage(deviceID uuid.UUID, filename string) error { // Get current device currentDevice, err := u.deviceDetailsRepo.GetByID(deviceID) if err != nil { return fmt.Errorf("device not found") } // Get all current image URLs allImageURLs := currentDevice.GetAllImageURLs() // Find the image URL that contains the filename var imageURLToDelete string var updatedImageURLs []string for _, imageURL := range allImageURLs { if strings.Contains(imageURL, filename) { imageURLToDelete = imageURL } else { updatedImageURLs = append(updatedImageURLs, imageURL) } } // Check if image was found if imageURLToDelete == "" { return fmt.Errorf("image not found in device") } // Don't allow deleting the last image if it's the primary image if len(allImageURLs) == 1 && currentDevice.ImageURL != nil && *currentDevice.ImageURL == imageURLToDelete { return fmt.Errorf("cannot delete the only remaining image") } // Prepare updates updates := map[string]interface{}{ "updated_at": time.Now(), } if len(updatedImageURLs) == 0 { // No images left updates["image_url"] = nil updates["image_urls"] = entity.StringSlice([]string{}) } else { // Update primary image if it was deleted if currentDevice.ImageURL != nil && *currentDevice.ImageURL == imageURLToDelete { updates["image_url"] = updatedImageURLs[0] // Set first remaining image as primary } // Update all images updates["image_urls"] = entity.StringSlice(updatedImageURLs) } // Update database err = u.deviceDetailsRepo.Update(deviceID, updates) if err != nil { return fmt.Errorf("failed to update device in database: %w", err) } // Delete the actual file from filesystem err = helper.DeleteDeviceImage(imageURLToDelete) if err != nil { // Log error but don't fail the request since database was already updated fmt.Printf("Warning: Failed to delete image file %s: %v\n", imageURLToDelete, err) } return nil } func (u *deviceDetailsUseCase) UpdateDeviceDetailsWithMultipleImages(id uuid.UUID, deviceDTO req.UpdateDeviceDetailsDTO, imageFiles []*multipart.FileHeader, replaceImages ...bool) error { err := u.validate.Struct(deviceDTO) if err != nil { return fmt.Errorf("validation error: %w", err) } updates := map[string]interface{}{} if deviceDTO.DeviceCode != nil { updates["device_code"] = *deviceDTO.DeviceCode } if deviceDTO.DeviceType != nil { updates["device_type"] = *deviceDTO.DeviceType } if deviceDTO.Longitude != nil { updates["longitude"] = *deviceDTO.Longitude } if deviceDTO.Latitude != nil { updates["latitude"] = *deviceDTO.Latitude } if deviceDTO.PortAmount != nil { if *deviceDTO.PortAmount < 0 { return fmt.Errorf("port amount cannot be negative") } if *deviceDTO.PortAmount > 0 { currentUsed, _, err := u.deviceDetailsRepo.GetPortUsageByDevice(id) if err != nil { return err } if *deviceDTO.PortAmount < currentUsed { return fmt.Errorf("cannot reduce port amount to %d, currently using %d ports", *deviceDTO.PortAmount, currentUsed) } } updates["port_amount"] = *deviceDTO.PortAmount } if deviceDTO.Status != nil { updates["status"] = *deviceDTO.Status } if deviceDTO.Region != nil { updates["region"] = *deviceDTO.Region } if deviceDTO.Province != nil { updates["province"] = *deviceDTO.Province } if deviceDTO.City != nil { updates["city"] = *deviceDTO.City } if deviceDTO.District != nil { updates["district"] = *deviceDTO.District } // Handle multiple image uploads if len(imageFiles) > 0 { // Get current device to handle existing images currentDevice, err := u.deviceDetailsRepo.GetByID(id) if err != nil { return err } // Save new images newImageURLs, err := helper.SaveDeviceImagesBulk(imageFiles) if err != nil { return err } var finalImageURLs []string shouldReplace := len(replaceImages) > 0 && replaceImages[0] if shouldReplace { // Replace all images - delete old ones for _, oldImageURL := range currentDevice.GetAllImageURLs() { helper.DeleteDeviceImage(oldImageURL) } finalImageURLs = newImageURLs } else { // Append to existing images existingImages := currentDevice.GetAllImageURLs() finalImageURLs = append(existingImages, newImageURLs...) } // Update primary image (first image in the final list) if len(finalImageURLs) > 0 && finalImageURLs[0] != "" { updates["image_url"] = finalImageURLs[0] } // Update all images using image_urls column (not additional_images) updates["image_urls"] = entity.StringSlice(finalImageURLs) } if len(updates) == 0 { return errors.New("no fields to update") } updates["updated_at"] = time.Now() return u.deviceDetailsRepo.Update(id, updates) } func (u *deviceDetailsUseCase) UpdateCustomerByPort(deviceID uuid.UUID, update req.UpdateCustomerByPortDTO) error { // Validate port number if update.PortNumber < 1 { return errors.New("port number must be greater than 0") } return u.deviceDetailsRepo.UpdateCustomerByPort(deviceID, update) } func (u *deviceDetailsUseCase) BulkUpdateCustomersByPort(deviceID uuid.UUID, updates []req.UpdateCustomerByPortDTO) error { if len(updates) == 0 { return errors.New("no updates provided") } // Validate all updates portNumbers := make(map[int]bool) newCustomerNames := make(map[string]bool) for _, update := range updates { // Check for duplicate port numbers in request if portNumbers[update.PortNumber] { return fmt.Errorf("duplicate port number in request: %d", update.PortNumber) } portNumbers[update.PortNumber] = true // Check for duplicate new customer names in request (ignore nulls) if update.NewCustomerName != nil && *update.NewCustomerName != "" { if newCustomerNames[*update.NewCustomerName] { return fmt.Errorf("duplicate new customer name in request: %s", *update.NewCustomerName) } newCustomerNames[*update.NewCustomerName] = true } // Validate port number if update.PortNumber < 1 { return fmt.Errorf("port number must be greater than 0: %d", update.PortNumber) } } return u.deviceDetailsRepo.BulkUpdateCustomersByPort(deviceID, updates) } func (u *deviceDetailsUseCase) RemoveCustomerByPort(deviceID uuid.UUID, portNumber int) error { if portNumber < 1 { return errors.New("port number must be greater than 0") } return u.deviceDetailsRepo.RemoveCustomerByPort(deviceID, portNumber) } func (u *deviceDetailsUseCase) AssignMultipleCustomersToPort(deviceID uuid.UUID, assignments []req.AssignMultipleCustomersDTO) error { if len(assignments) == 0 { return errors.New("no assignments provided") } // Validate all assignments first customerNames := make(map[string]bool) portNumbers := make(map[int]bool) for _, assignment := range assignments { // Check for duplicate customer names in the request if customerNames[assignment.CustomerName] { return fmt.Errorf("duplicate customer name in request: %s", assignment.CustomerName) } customerNames[assignment.CustomerName] = true // Check for duplicate port numbers in the request if assignment.PortNumber != nil { if portNumbers[*assignment.PortNumber] { return fmt.Errorf("duplicate port number in request: %d", *assignment.PortNumber) } portNumbers[*assignment.PortNumber] = true } } return u.deviceDetailsRepo.AssignMultipleCustomersToPort(deviceID, assignments) } func (u *deviceDetailsUseCase) UpdatePortAssignments(deviceID uuid.UUID, assignments []req.PortAssignmentDTO) error { // Validate the assignments if len(assignments) == 0 { return errors.New("port assignments cannot be empty") } // Check for duplicate port numbers portNumbers := make(map[int]bool) for _, assignment := range assignments { if portNumbers[assignment.PortNumber] { return fmt.Errorf("duplicate port number: %d", assignment.PortNumber) } portNumbers[assignment.PortNumber] = true } return u.deviceDetailsRepo.UpdatePortAssignments(deviceID, assignments) } func (u *deviceDetailsUseCase) UpdatePortUsage(deviceID uuid.UUID, portUsed int) error { return u.deviceDetailsRepo.UpdatePortUsage(deviceID, portUsed) } func (u *deviceDetailsUseCase) CreateDeviceDetails(deviceDTO req.DeviceDetailsDTO) error { err := u.validate.Struct(deviceDTO) if err != nil { return fmt.Errorf("validation error: %w", err) } newDevice := entity.Device{ ID: uuid.New(), DeviceCode: deviceDTO.DeviceCode, DeviceType: entity.DeviceType(deviceDTO.DeviceType), Longitude: deviceDTO.Longitude, Latitude: deviceDTO.Latitude, PortAmount: deviceDTO.PortAmount, Status: entity.DeviceStatus(deviceDTO.Status), Province: deviceDTO.Province, City: deviceDTO.City, District: deviceDTO.District, CreatedAt: time.Now(), UpdatedAt: time.Now(), } return u.deviceDetailsRepo.Create(newDevice) } func (u *deviceDetailsUseCase) GetAllDeviceDetails() ([]res.DeviceDetailsResponse, error) { devices, err := u.deviceDetailsRepo.GetAll() if err != nil { return nil, err } return helper.ConvertToDeviceDetailsResponses(devices, u.geocoder) } func (u *deviceDetailsUseCase) GetDeviceDetailsByID(id uuid.UUID) (res.DeviceDetailsResponse, error) { device, err := u.deviceDetailsRepo.GetByID(id) if err != nil { return res.DeviceDetailsResponse{}, err } return helper.ConvertToDeviceDetailsResponse(device, u.geocoder) } func (u *deviceDetailsUseCase) UpdateDeviceDetails(id uuid.UUID, deviceDTO req.UpdateDeviceDetailsDTO) error { err := u.validate.Struct(deviceDTO) if err != nil { return fmt.Errorf("validation error: %w", err) } updates := map[string]interface{}{} if deviceDTO.DeviceCode != nil { updates["device_code"] = *deviceDTO.DeviceCode } if deviceDTO.DeviceType != nil { updates["device_type"] = *deviceDTO.DeviceType } if deviceDTO.Longitude != nil { updates["longitude"] = *deviceDTO.Longitude } if deviceDTO.Latitude != nil { updates["latitude"] = *deviceDTO.Latitude } if deviceDTO.PortAmount != nil { // Validate port amount change if *deviceDTO.PortAmount < 0 { return fmt.Errorf("port amount cannot be negative") } // If not setting to 0, check current usage if *deviceDTO.PortAmount > 0 { currentUsed, _, err := u.deviceDetailsRepo.GetPortUsageByDevice(id) if err != nil { return err } if *deviceDTO.PortAmount < currentUsed { return fmt.Errorf("cannot reduce port amount to %d, currently using %d ports", *deviceDTO.PortAmount, currentUsed) } } updates["port_amount"] = *deviceDTO.PortAmount } if deviceDTO.Status != nil { updates["status"] = *deviceDTO.Status } if deviceDTO.Region != nil { updates["region"] = *deviceDTO.Region } if deviceDTO.Province != nil { updates["province"] = *deviceDTO.Province } if deviceDTO.City != nil { updates["city"] = *deviceDTO.City } if deviceDTO.District != nil { updates["district"] = *deviceDTO.District } if len(updates) == 0 { return errors.New("no fields to update") } updates["updated_at"] = time.Now() return u.deviceDetailsRepo.Update(id, updates) } func (u *deviceDetailsUseCase) DeleteDeviceDetails(id uuid.UUID) error { // Check if device has connections backbones, err := u.deviceDetailsRepo.GetBackbonesByDeviceID(id) if err != nil { return err } if len(backbones) > 0 { return errors.New("cannot delete device with active backbone connections") } fishbones, err := u.deviceDetailsRepo.GetFishbonesByDeviceID(id) if err != nil { return err } if len(fishbones) > 0 { return errors.New("cannot delete device with active fishbone connections") } towers, err := u.deviceDetailsRepo.GetTowersByDeviceID(id) if err != nil { return err } if len(towers) > 0 { return errors.New("cannot delete device with active tower connections") } return u.deviceDetailsRepo.Delete(id) } func (u *deviceDetailsUseCase) RecalculatePortUsage(deviceID uuid.UUID) error { return u.deviceDetailsRepo.UpdateDevicePortUsage(deviceID) } func (u *deviceDetailsUseCase) AssignCustomerToPort(deviceID uuid.UUID, customerName string, portNumber *int) error { return u.deviceDetailsRepo.AssignCustomerToPort(deviceID, customerName, portNumber) }