NAM-APJATEL-BACKEND/usecase/device_details.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)
}