523 lines
18 KiB
Go
523 lines
18 KiB
Go
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
|
|
GetDevicesWithoutTowers(deviceTypes []string) ([]res.DeviceDetailsResponse, error)
|
|
GetDevicesWithoutConnections(deviceTypes []string) ([]res.DeviceDetailsResponse, 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) GetDevicesWithoutConnections(deviceTypes []string) ([]res.DeviceDetailsResponse, error) {
|
|
// Validate device types
|
|
validTypes := map[string]bool{"closure": true, "OTB": true}
|
|
for _, deviceType := range deviceTypes {
|
|
if !validTypes[deviceType] {
|
|
return nil, fmt.Errorf("invalid device type: %s. Only closure and OTB are allowed", deviceType)
|
|
}
|
|
}
|
|
|
|
// If no device types provided, default to closure and OTB
|
|
if len(deviceTypes) == 0 {
|
|
deviceTypes = []string{"closure", "OTB"}
|
|
}
|
|
|
|
devices, err := u.deviceDetailsRepo.GetDevicesWithoutConnections(deviceTypes)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
responses, err := helper.ConvertToDeviceDetailsResponses(devices, u.geocoder)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return responses, nil
|
|
}
|
|
|
|
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) GetDevicesWithoutTowers(deviceTypes []string) ([]res.DeviceDetailsResponse, error) {
|
|
// Validate device types
|
|
validTypes := map[string]bool{"ODP": true, "OTB": true}
|
|
for _, deviceType := range deviceTypes {
|
|
if !validTypes[deviceType] {
|
|
return nil, fmt.Errorf("invalid device type: %s. Only ODP and OTB are allowed", deviceType)
|
|
}
|
|
}
|
|
|
|
// If no device types provided, default to ODP and OTB
|
|
if len(deviceTypes) == 0 {
|
|
deviceTypes = []string{"ODP", "OTB"}
|
|
}
|
|
|
|
devices, err := u.deviceDetailsRepo.GetDevicesWithoutTowers(deviceTypes)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
responses, err := helper.ConvertToDeviceDetailsResponses(devices, u.geocoder)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return responses, 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)
|
|
} |