NAM-APJATEL-BACKEND/usecase/device_usecase.go

1131 lines
33 KiB
Go

package usecase
import (
"fmt"
"mime/multipart"
"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 DeviceUseCase interface {
CreateDevice(device req.DeviceDTO) error
GetAllDevices() ([]res.DeviceResponse, error)
GetByID(id uuid.UUID) (res.DeviceResponse, error)
UpdateDevice(id uuid.UUID, device req.UpdateDeviceDTO) error
GetByType(deviceType string) ([]res.DeviceTypeResponse, error)
BulkUploadImages(devices []req.BulkDeviceImageUploadDTO, imageFiles []*multipart.FileHeader) error
CreateDeviceWithMultipleImages(device req.DeviceDTO, imageFiles []*multipart.FileHeader) error
UpdateDeviceWithMultipleImages(id uuid.UUID, device req.UpdateDeviceDTO, imageFiles []*multipart.FileHeader, replaceImages ...bool) error
BulkUploadImagesMultiple(devices []req.BulkDeviceImageUploadDTO, imageFiles []*multipart.FileHeader, fileDistribution []int) error
ValidateTowerExists(towerID uuid.UUID) (bool, error)
ValidateOLTExists(oltID uuid.UUID) (bool, error)
// Bulk operations
BulkCreateDevices(request req.BulkCreateDeviceDTO) (res.BulkDeviceOperationResponse, error)
BulkCreateDevicesWithImages(request req.BulkCreateDeviceDTO, imageFiles []*multipart.FileHeader, imageIndexes []int) (res.BulkDeviceOperationResponse, error)
BulkUpdateDevices(request req.BulkUpdateDeviceDTO) (res.BulkDeviceOperationResponse, error)
BulkUpdateDevicesWithImages(request req.BulkUpdateDeviceDTO, imageFiles []*multipart.FileHeader, imageIndexes []int, replaceImages bool) (res.BulkDeviceOperationResponse, error)
BulkDeleteDevices(request req.BulkDeleteDeviceDTO) (res.BulkDeviceOperationResponse, error)
}
type deviceUseCase struct {
deviceRepo repository.DevicesRepo
oltRepo repository.OLTRepo
validate *validator.Validate
geocoder service.GeocodingService
}
func NewDeviceUseCase(deviceRepo repository.DevicesRepo, oltRepo repository.OLTRepo, geocoder service.GeocodingService) DeviceUseCase {
return &deviceUseCase{
deviceRepo: deviceRepo,
oltRepo: oltRepo,
geocoder: geocoder,
validate: validator.New(),
}
}
func (u *deviceUseCase) ValidateOLTExists(oltID uuid.UUID) (bool, error) {
_, err := u.oltRepo.GetByID(oltID)
if err != nil {
return false, nil
}
return true, nil
}
func (u *deviceUseCase) ValidateTowerExists(towerID uuid.UUID) (bool, error) {
return u.deviceRepo.ValidateTowerExists(towerID)
}
func (u *deviceUseCase) CreateDeviceWithMultipleImages(device req.DeviceDTO, imageFiles []*multipart.FileHeader) error {
err := u.validate.Struct(device)
if err != nil {
return fmt.Errorf("validation error: %w", err)
}
if device.DeviceType == "OTB" || device.DeviceType == "ODP" {
if device.PortAmount <= 0 {
return fmt.Errorf("port amount must be greater than 0 for OTB or ODP devices")
}
}
if device.TowerID != nil {
towerExists, err := u.ValidateTowerExists(*device.TowerID)
if err != nil {
return fmt.Errorf("failed to validate tower: %w", err)
}
if !towerExists {
return fmt.Errorf("tower with ID %s not found", device.TowerID.String())
}
}
if device.OLTID != nil {
// Only ODP devices can be assigned to OLT
if device.DeviceType != "ODP" {
return fmt.Errorf("only ODP devices can be assigned to OLT")
}
oltExists, err := u.ValidateOLTExists(*device.OLTID)
if err != nil {
return fmt.Errorf("failed to validate OLT: %w", err)
}
if !oltExists {
return fmt.Errorf("OLT with ID %s not found", device.OLTID.String())
}
}
var imageURLs []string
var primaryImageURL string
if len(imageFiles) > 0 {
// Save all images
imageURLs, err = helper.SaveDeviceImagesBulk(imageFiles)
if err != nil {
return err
}
// First image becomes primary
if len(imageURLs) > 0 && imageURLs[0] != "" {
primaryImageURL = imageURLs[0]
}
}
newDevice := entity.Device{
ID: uuid.New(),
DeviceCode: device.DeviceCode,
DeviceType: entity.DeviceType(device.DeviceType),
Longitude: device.Longitude,
Latitude: device.Latitude,
PortAmount: device.PortAmount,
Status: entity.DeviceStatus(device.Status),
Province: device.Province,
City: device.City,
District: device.District,
TowerID: device.TowerID, // Add TowerID field
OLTID: device.OLTID,
ImageURL: &primaryImageURL, // Primary image as pointer
ImageURLs: entity.StringSlice(imageURLs), // All images
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
}
return u.deviceRepo.Post(newDevice)
}
func (u *deviceUseCase) UpdateDeviceWithMultipleImages(id uuid.UUID, device req.UpdateDeviceDTO, imageFiles []*multipart.FileHeader, replaceImages ...bool) error {
err := u.validate.Struct(device)
if err != nil {
return fmt.Errorf("validation error: %w", err)
}
updates := map[string]interface{}{}
if device.DeviceCode != nil {
updates["DeviceCode"] = *device.DeviceCode
}
if device.DeviceType != nil {
updates["DeviceType"] = *device.DeviceType
}
if device.Longitude != nil {
updates["Longitude"] = *device.Longitude
}
if device.Latitude != nil {
updates["Latitude"] = *device.Latitude
}
if device.PortAmount != nil {
updates["PortAmount"] = *device.PortAmount
}
if device.Status != nil {
updates["Status"] = *device.Status
}
if device.Province != nil {
updates["Province"] = *device.Province
}
if device.City != nil {
updates["City"] = *device.City
}
if device.District != nil {
updates["District"] = *device.District
}
// Handle TowerID validation and update
if device.TowerID != nil {
towerExists, err := u.ValidateTowerExists(*device.TowerID)
if err != nil {
return fmt.Errorf("failed to validate tower: %w", err)
}
if !towerExists {
return fmt.Errorf("tower with ID %s not found", device.TowerID.String())
}
updates["TowerID"] = *device.TowerID
}
// Handle OLTID validation and update
if device.OLTID != nil {
// Get current device to check type
currentDevice, err := u.deviceRepo.GetByID(id)
if err != nil {
return fmt.Errorf("device not found: %w", err)
}
// Only ODP devices can be assigned to OLT
deviceType := string(currentDevice.DeviceType)
if device.DeviceType != nil {
deviceType = *device.DeviceType
}
if deviceType != "ODP" {
return fmt.Errorf("only ODP devices can be assigned to OLT")
}
oltExists, err := u.ValidateOLTExists(*device.OLTID)
if err != nil {
return fmt.Errorf("failed to validate OLT: %w", err)
}
if !oltExists {
return fmt.Errorf("OLT with ID %s not found", device.OLTID.String())
}
updates["OLTID"] = *device.OLTID
}
// Handle multiple image uploads
if len(imageFiles) > 0 {
// Get current device to handle existing images
currentDevice, err := u.deviceRepo.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["ImageURL"] = finalImageURLs[0]
}
// Update all images
updates["ImageURLs"] = entity.StringSlice(finalImageURLs)
}
if device.DeviceType != nil && (*device.DeviceType == "OTB" || *device.DeviceType == "ODP") && device.PortAmount != nil && *device.PortAmount <= 0 {
return fmt.Errorf("port amount must be greater than 0 for OTB or ODP devices")
}
if len(updates) == 0 {
return fmt.Errorf("no update data")
}
updates["UpdatedAt"] = time.Now()
return u.deviceRepo.Update(id, updates)
}
func (u *deviceUseCase) BulkUploadImagesMultiple(devices []req.BulkDeviceImageUploadDTO, imageFiles []*multipart.FileHeader, fileDistribution []int) error {
if len(devices) == 0 {
return fmt.Errorf("no devices provided")
}
if len(fileDistribution) != len(devices) {
return fmt.Errorf("file distribution count must match device count")
}
// Validate all device IDs exist
for i, device := range devices {
_, err := u.deviceRepo.GetByID(device.DeviceID)
if err != nil {
return fmt.Errorf("device %d with ID %s not found", i, device.DeviceID)
}
}
// Save all images
imageURLs, err := helper.SaveDeviceImagesBulk(imageFiles)
if err != nil {
return fmt.Errorf("failed to save images: %w", err)
}
// Distribute images to devices
updates := make(map[uuid.UUID][]string)
fileIndex := 0
for i, device := range devices {
imageCount := fileDistribution[i]
deviceImages := make([]string, 0)
for j := 0; j < imageCount && fileIndex < len(imageURLs); j++ {
if imageURLs[fileIndex] != "" {
deviceImages = append(deviceImages, imageURLs[fileIndex])
}
fileIndex++
}
if len(deviceImages) > 0 {
updates[device.DeviceID] = deviceImages
}
}
// Update database
return u.deviceRepo.BulkUpdateImagesMultiple(updates)
}
// Keep old methods for backward compatibility
func (u *deviceUseCase) CreateDevice(device req.DeviceDTO) error {
return u.CreateDeviceWithMultipleImages(device, nil)
}
func (u *deviceUseCase) GetAllDevices() ([]res.DeviceResponse, error) {
devices, err := u.deviceRepo.GetAll()
if err != nil {
return []res.DeviceResponse{}, err
}
devicesResponse, err := helper.ConvertToDeviceResponse(devices, u.geocoder)
if err != nil {
return []res.DeviceResponse{}, err
}
return devicesResponse, nil
}
func (u *deviceUseCase) BulkUploadImages(devices []req.BulkDeviceImageUploadDTO, imageFiles []*multipart.FileHeader) error {
// Default to one image per device
fileDistribution := make([]int, len(devices))
for i := range fileDistribution {
fileDistribution[i] = 1
}
return u.BulkUploadImagesMultiple(devices, imageFiles, fileDistribution)
}
func (u *deviceUseCase) GetByID(id uuid.UUID) (res.DeviceResponse, error) {
device, err := u.deviceRepo.GetByID(id)
if err != nil {
return res.DeviceResponse{}, err
}
deviceResp, err := helper.ConvertToDeviceResponseId(device, u.geocoder)
if err != nil {
return res.DeviceResponse{}, err
}
return deviceResp, nil
}
func (u *deviceUseCase) UpdateDevice(id uuid.UUID, device req.UpdateDeviceDTO) error {
err := u.validate.Struct(device)
if err != nil {
return fmt.Errorf("validation error: %w", err)
}
updates := map[string]interface{}{}
if device.DeviceCode != nil {
updates["DeviceCode"] = *device.DeviceCode
}
if device.DeviceType != nil {
updates["DeviceType"] = *device.DeviceType
}
if device.Longitude != nil {
updates["Longitude"] = *device.Longitude
}
if device.Latitude != nil {
updates["Latitude"] = *device.Latitude
}
if device.PortAmount != nil {
updates["PortAmount"] = *device.PortAmount
}
if device.Status != nil {
updates["Status"] = *device.Status
}
if device.Province != nil {
updates["Province"] = *device.Province
}
if device.City != nil {
updates["City"] = *device.City
}
if device.District != nil {
updates["District"] = *device.District
}
// Handle TowerID validation and update
if device.TowerID != nil {
towerExists, err := u.ValidateTowerExists(*device.TowerID)
if err != nil {
return fmt.Errorf("failed to validate tower: %w", err)
}
if !towerExists {
return fmt.Errorf("tower with ID %s not found", device.TowerID.String())
}
updates["TowerID"] = *device.TowerID
}
// Handle OLTID validation and update
if device.OLTID != nil {
// Get current device to check type
currentDevice, err := u.deviceRepo.GetByID(id)
if err != nil {
return fmt.Errorf("device not found: %w", err)
}
// Only ODP devices can be assigned to OLT
deviceType := string(currentDevice.DeviceType)
if device.DeviceType != nil {
deviceType = *device.DeviceType
}
if deviceType != "ODP" {
return fmt.Errorf("only ODP devices can be assigned to OLT")
}
oltExists, err := u.ValidateOLTExists(*device.OLTID)
if err != nil {
return fmt.Errorf("failed to validate OLT: %w", err)
}
if !oltExists {
return fmt.Errorf("OLT with ID %s not found", device.OLTID.String())
}
updates["OLTID"] = *device.OLTID
}
if device.DeviceType != nil && (*device.DeviceType == "OTB" || *device.DeviceType == "ODP") && device.PortAmount != nil && *device.PortAmount <= 0 {
return fmt.Errorf("port amount must be greater than 0 for OTB or ODP devices")
}
if len(updates) == 0 {
return fmt.Errorf("no update data")
}
updates["UpdatedAt"] = time.Now()
return u.deviceRepo.Update(id, updates)
}
func (u *deviceUseCase) GetByType(deviceType string) ([]res.DeviceTypeResponse, error) {
devices, err := u.deviceRepo.GetByType(deviceType)
if err != nil {
return nil, err
}
deviceTypeResponses := helper.ConvertToDeviceTypeResponse(devices)
return deviceTypeResponses, nil
}
// BulkCreateDevices creates multiple devices at once
func (u *deviceUseCase) BulkCreateDevices(request req.BulkCreateDeviceDTO) (res.BulkDeviceOperationResponse, error) {
startTime := time.Now()
err := u.validate.Struct(request)
if err != nil {
return res.BulkDeviceOperationResponse{}, fmt.Errorf("validation error: %w", err)
}
var devices []entity.Device
var errors []res.BulkDeviceError
// Validate each device
for i, deviceReq := range request.Devices {
if err := u.validate.Struct(deviceReq); err != nil {
errors = append(errors, res.BulkDeviceError{
Index: i,
Error: "Validation failed",
Details: err.Error(),
})
continue
}
// Validate port amount for OTB/ODP devices
if deviceReq.DeviceType == "OTB" || deviceReq.DeviceType == "ODP" {
if deviceReq.PortAmount <= 0 {
errors = append(errors, res.BulkDeviceError{
Index: i,
Error: "Invalid port amount",
Details: "port amount must be greater than 0 for OTB or ODP devices",
})
continue
}
}
// Validate tower if provided
if deviceReq.TowerID != nil {
towerExists, err := u.ValidateTowerExists(*deviceReq.TowerID)
if err != nil || !towerExists {
errors = append(errors, res.BulkDeviceError{
Index: i,
Error: "Tower not found",
Details: fmt.Sprintf("tower with ID %s not found", deviceReq.TowerID.String()),
})
continue
}
}
// Validate OLT if provided
if deviceReq.OLTID != nil {
if deviceReq.DeviceType != "ODP" {
errors = append(errors, res.BulkDeviceError{
Index: i,
Error: "Invalid OLT assignment",
Details: "only ODP devices can be assigned to OLT",
})
continue
}
oltExists, err := u.ValidateOLTExists(*deviceReq.OLTID)
if err != nil || !oltExists {
errors = append(errors, res.BulkDeviceError{
Index: i,
Error: "OLT not found",
Details: fmt.Sprintf("OLT with ID %s not found", deviceReq.OLTID.String()),
})
continue
}
}
device := entity.Device{
ID: uuid.New(),
DeviceCode: deviceReq.DeviceCode,
DeviceType: entity.DeviceType(deviceReq.DeviceType),
Longitude: deviceReq.Longitude,
Latitude: deviceReq.Latitude,
PortAmount: deviceReq.PortAmount,
Status: entity.DeviceStatus(deviceReq.Status),
Province: deviceReq.Province,
City: deviceReq.City,
District: deviceReq.District,
TowerID: deviceReq.TowerID,
OLTID: deviceReq.OLTID,
}
devices = append(devices, device)
}
// Bulk insert valid devices
var createdDevices []entity.Device
if len(devices) > 0 {
createdDevices, _ = u.deviceRepo.BulkCreate(devices)
}
// Fetch created devices
var responses []res.DeviceResponse
for _, dev := range createdDevices {
fetchedDevice, err := u.deviceRepo.GetByID(dev.ID)
if err == nil {
// Build address from location fields
address := ""
if fetchedDevice.City != nil && fetchedDevice.Province != nil {
address = *fetchedDevice.City + ", " + *fetchedDevice.Province
} else if fetchedDevice.City != nil {
address = *fetchedDevice.City
} else if fetchedDevice.Province != nil {
address = *fetchedDevice.Province
}
responses = append(responses, res.DeviceResponse{
ID: fetchedDevice.ID,
DeviceCode: fetchedDevice.DeviceCode,
DeviceType: string(fetchedDevice.DeviceType),
Longitude: fetchedDevice.Longitude,
Latitude: fetchedDevice.Latitude,
PortAmount: fetchedDevice.PortAmount,
Status: string(fetchedDevice.Status),
Address: address,
Region: nil,
Province: fetchedDevice.Province,
City: fetchedDevice.City,
District: fetchedDevice.District,
ImageURL: fetchedDevice.ImageURL,
ImageURLs: fetchedDevice.ImageURLs,
CreatedAt: fetchedDevice.CreatedAt,
UpdatedAt: fetchedDevice.UpdatedAt,
})
}
}
executionTime := time.Since(startTime).String()
return res.BulkDeviceOperationResponse{
TotalRequested: len(request.Devices),
Successful: len(createdDevices),
Failed: len(errors),
Errors: errors,
Results: responses,
ExecutionTime: executionTime,
}, nil
}
// BulkUpdateDevices updates multiple devices with the same values
func (u *deviceUseCase) BulkUpdateDevices(request req.BulkUpdateDeviceDTO) (res.BulkDeviceOperationResponse, error) {
startTime := time.Now()
err := u.validate.Struct(request)
if err != nil {
return res.BulkDeviceOperationResponse{}, fmt.Errorf("validation error: %w", err)
}
// Validate that devices exist
var validIDs []uuid.UUID
var errors []res.BulkDeviceError
for i, id := range request.DeviceIDs {
_, err := u.deviceRepo.GetByID(id)
if err != nil {
errors = append(errors, res.BulkDeviceError{
Index: i,
Error: "Device not found",
Details: id.String(),
})
continue
}
validIDs = append(validIDs, id)
}
// Build update map
updates := make(map[string]interface{})
if request.Updates.DeviceCode != nil {
updates["device_code"] = *request.Updates.DeviceCode
}
if request.Updates.DeviceType != nil {
updates["device_type"] = *request.Updates.DeviceType
}
if request.Updates.Longitude != nil {
updates["longitude"] = *request.Updates.Longitude
}
if request.Updates.Latitude != nil {
updates["latitude"] = *request.Updates.Latitude
}
if request.Updates.PortAmount != nil {
updates["port_amount"] = *request.Updates.PortAmount
}
if request.Updates.Status != nil {
updates["status"] = *request.Updates.Status
}
if request.Updates.Province != nil {
updates["province"] = *request.Updates.Province
}
if request.Updates.City != nil {
updates["city"] = *request.Updates.City
}
if request.Updates.District != nil {
updates["district"] = *request.Updates.District
}
if request.Updates.TowerID != nil {
updates["tower_id"] = *request.Updates.TowerID
}
if request.Updates.OLTID != nil {
updates["olt_id"] = *request.Updates.OLTID
}
// Perform bulk update
var rowsAffected int64
if len(validIDs) > 0 && len(updates) > 0 {
rowsAffected, err = u.deviceRepo.BulkUpdate(validIDs, updates)
if err != nil {
return res.BulkDeviceOperationResponse{}, fmt.Errorf("bulk update failed: %w", err)
}
}
// Fetch updated devices
var responses []res.DeviceResponse
for _, id := range validIDs {
device, err := u.deviceRepo.GetByID(id)
if err == nil {
// Build address from location fields
address := ""
if device.City != nil && device.Province != nil {
address = *device.City + ", " + *device.Province
} else if device.City != nil {
address = *device.City
} else if device.Province != nil {
address = *device.Province
}
responses = append(responses, res.DeviceResponse{
ID: device.ID,
DeviceCode: device.DeviceCode,
DeviceType: string(device.DeviceType),
Longitude: device.Longitude,
Latitude: device.Latitude,
PortAmount: device.PortAmount,
Status: string(device.Status),
Address: address,
Region: nil,
Province: device.Province,
City: device.City,
District: device.District,
ImageURL: device.ImageURL,
ImageURLs: device.ImageURLs,
CreatedAt: device.CreatedAt,
UpdatedAt: device.UpdatedAt,
})
}
}
executionTime := time.Since(startTime).String()
return res.BulkDeviceOperationResponse{
TotalRequested: len(request.DeviceIDs),
Successful: int(rowsAffected),
Failed: len(errors),
Errors: errors,
Results: responses,
ExecutionTime: executionTime,
}, nil
}
// BulkDeleteDevices deletes multiple devices
func (u *deviceUseCase) BulkDeleteDevices(request req.BulkDeleteDeviceDTO) (res.BulkDeviceOperationResponse, error) {
startTime := time.Now()
err := u.validate.Struct(request)
if err != nil {
return res.BulkDeviceOperationResponse{}, fmt.Errorf("validation error: %w", err)
}
// Validate that devices exist
var validIDs []uuid.UUID
var errors []res.BulkDeviceError
for i, id := range request.DeviceIDs {
_, err := u.deviceRepo.GetByID(id)
if err != nil {
errors = append(errors, res.BulkDeviceError{
Index: i,
Error: "Device not found",
Details: id.String(),
})
continue
}
validIDs = append(validIDs, id)
}
// Perform bulk delete
var rowsAffected int64
if len(validIDs) > 0 {
rowsAffected, err = u.deviceRepo.BulkDelete(validIDs)
if err != nil {
return res.BulkDeviceOperationResponse{}, fmt.Errorf("bulk delete failed: %w", err)
}
}
executionTime := time.Since(startTime).String()
return res.BulkDeviceOperationResponse{
TotalRequested: len(request.DeviceIDs),
Successful: int(rowsAffected),
Failed: len(errors),
Errors: errors,
ExecutionTime: executionTime,
}, nil
}
// BulkCreateDevicesWithImages creates multiple devices with their images
func (u *deviceUseCase) BulkCreateDevicesWithImages(request req.BulkCreateDeviceDTO, imageFiles []*multipart.FileHeader, imageIndexes []int) (res.BulkDeviceOperationResponse, error) {
startTime := time.Now()
err := u.validate.Struct(request)
if err != nil {
return res.BulkDeviceOperationResponse{}, fmt.Errorf("validation error: %w", err)
}
// Validate image indexes
if len(imageIndexes) != len(request.Devices) {
return res.BulkDeviceOperationResponse{}, fmt.Errorf("image_indexes length (%d) must match devices length (%d)", len(imageIndexes), len(request.Devices))
}
// Calculate total expected images
totalExpectedImages := 0
for _, count := range imageIndexes {
totalExpectedImages += count
}
if totalExpectedImages != len(imageFiles) {
return res.BulkDeviceOperationResponse{}, fmt.Errorf("total images (%d) doesn't match sum of image_indexes (%d)", len(imageFiles), totalExpectedImages)
}
var createdDevices []entity.Device
var errors []res.BulkDeviceError
var responses []res.DeviceResponse
// Process each device with its images
imageOffset := 0
for i, deviceReq := range request.Devices {
// Validate device type
if deviceReq.DeviceType != "ODP" && deviceReq.DeviceType != "OTB" && deviceReq.DeviceType != "closure" {
errors = append(errors, res.BulkDeviceError{
Index: i,
Error: "Invalid device type",
Details: fmt.Sprintf("Type must be ODP, OTB, or closure, got: %s", deviceReq.DeviceType),
})
continue
}
// Validate port amount for OTB and ODP
if (deviceReq.DeviceType == "OTB" || deviceReq.DeviceType == "ODP") && deviceReq.PortAmount <= 0 {
errors = append(errors, res.BulkDeviceError{
Index: i,
Error: "Invalid port amount",
Details: fmt.Sprintf("Port amount must be > 0 for %s devices", deviceReq.DeviceType),
})
continue
}
// Validate tower if provided
if deviceReq.TowerID != nil {
towerExists, err := u.ValidateTowerExists(*deviceReq.TowerID)
if err != nil || !towerExists {
errors = append(errors, res.BulkDeviceError{
Index: i,
Error: "Tower not found",
Details: deviceReq.TowerID.String(),
})
continue
}
}
// Validate OLT if provided
if deviceReq.OLTID != nil {
if deviceReq.DeviceType != "ODP" {
errors = append(errors, res.BulkDeviceError{
Index: i,
Error: "Invalid OLT assignment",
Details: "Only ODP devices can be assigned to OLT",
})
continue
}
oltExists, err := u.ValidateOLTExists(*deviceReq.OLTID)
if err != nil || !oltExists {
errors = append(errors, res.BulkDeviceError{
Index: i,
Error: "OLT not found",
Details: deviceReq.OLTID.String(),
})
continue
}
}
// Get images for this device
deviceImageCount := imageIndexes[i]
var deviceImages []*multipart.FileHeader
if deviceImageCount > 0 {
deviceImages = imageFiles[imageOffset : imageOffset+deviceImageCount]
imageOffset += deviceImageCount
}
// Save images
var imageURLs []string
var primaryImageURL string
if len(deviceImages) > 0 {
imageURLs, err = helper.SaveDeviceImagesBulk(deviceImages)
if err != nil {
errors = append(errors, res.BulkDeviceError{
Index: i,
Error: "Failed to save images",
Details: err.Error(),
})
continue
}
if len(imageURLs) > 0 && imageURLs[0] != "" {
primaryImageURL = imageURLs[0]
}
}
// Create device
device := entity.Device{
ID: uuid.New(),
DeviceCode: deviceReq.DeviceCode,
DeviceType: entity.DeviceType(deviceReq.DeviceType),
Longitude: deviceReq.Longitude,
Latitude: deviceReq.Latitude,
PortAmount: deviceReq.PortAmount,
Status: entity.DeviceStatus(deviceReq.Status),
Province: deviceReq.Province,
City: deviceReq.City,
District: deviceReq.District,
TowerID: deviceReq.TowerID,
OLTID: deviceReq.OLTID,
ImageURL: &primaryImageURL,
ImageURLs: entity.StringSlice(imageURLs),
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
}
createdDevices = append(createdDevices, device)
}
// Bulk insert valid devices
if len(createdDevices) > 0 {
successDevices, bulkErrors := u.deviceRepo.BulkCreate(createdDevices)
if len(bulkErrors) > 0 {
// Add repository errors to our error list
for idx, bulkErr := range bulkErrors {
if bulkErr != nil {
errors = append(errors, res.BulkDeviceError{
Index: idx,
Error: "Failed to create device",
Details: bulkErr.Error(),
})
}
}
}
// Build responses from successfully created devices
for _, device := range successDevices {
fetchedDevice, err := u.deviceRepo.GetByID(device.ID)
if err != nil {
continue
}
address := ""
if fetchedDevice.City != nil && fetchedDevice.Province != nil {
address = *fetchedDevice.City + ", " + *fetchedDevice.Province
} else if fetchedDevice.City != nil {
address = *fetchedDevice.City
} else if fetchedDevice.Province != nil {
address = *fetchedDevice.Province
}
responses = append(responses, res.DeviceResponse{
ID: fetchedDevice.ID,
DeviceCode: fetchedDevice.DeviceCode,
DeviceType: string(fetchedDevice.DeviceType),
Longitude: fetchedDevice.Longitude,
Latitude: fetchedDevice.Latitude,
PortAmount: fetchedDevice.PortAmount,
Status: string(fetchedDevice.Status),
Address: address,
Region: nil,
Province: fetchedDevice.Province,
City: fetchedDevice.City,
District: fetchedDevice.District,
ImageURL: fetchedDevice.ImageURL,
ImageURLs: fetchedDevice.ImageURLs,
CreatedAt: fetchedDevice.CreatedAt,
UpdatedAt: fetchedDevice.UpdatedAt,
})
}
}
executionTime := time.Since(startTime).String()
return res.BulkDeviceOperationResponse{
TotalRequested: len(request.Devices),
Successful: len(createdDevices),
Failed: len(errors),
Errors: errors,
Results: responses,
ExecutionTime: executionTime,
}, nil
}
// BulkUpdateDevicesWithImages updates multiple devices with images
func (u *deviceUseCase) BulkUpdateDevicesWithImages(request req.BulkUpdateDeviceDTO, imageFiles []*multipart.FileHeader, imageIndexes []int, replaceImages bool) (res.BulkDeviceOperationResponse, error) {
startTime := time.Now()
err := u.validate.Struct(request)
if err != nil {
return res.BulkDeviceOperationResponse{}, fmt.Errorf("validation error: %w", err)
}
// Validate image indexes
if len(imageIndexes) != len(request.DeviceIDs) {
return res.BulkDeviceOperationResponse{}, fmt.Errorf("image_indexes length (%d) must match device_ids length (%d)", len(imageIndexes), len(request.DeviceIDs))
}
// Calculate total expected images
totalExpectedImages := 0
for _, count := range imageIndexes {
totalExpectedImages += count
}
if totalExpectedImages != len(imageFiles) {
return res.BulkDeviceOperationResponse{}, fmt.Errorf("total images (%d) doesn't match sum of image_indexes (%d)", len(imageFiles), totalExpectedImages)
}
var updatedCount int
var errors []res.BulkDeviceError
var responses []res.DeviceResponse
// Process each device with its images
imageOffset := 0
for i, deviceID := range request.DeviceIDs {
// Check if device exists
existingDevice, err := u.deviceRepo.GetByID(deviceID)
if err != nil {
errors = append(errors, res.BulkDeviceError{
Index: i,
Error: "Device not found",
Details: deviceID.String(),
})
continue
}
// Get images for this device
deviceImageCount := imageIndexes[i]
var deviceImages []*multipart.FileHeader
if deviceImageCount > 0 {
deviceImages = imageFiles[imageOffset : imageOffset+deviceImageCount]
imageOffset += deviceImageCount
}
// Build update map
updates := make(map[string]interface{})
if request.Updates.DeviceCode != nil {
updates["device_code"] = *request.Updates.DeviceCode
}
if request.Updates.DeviceType != nil {
updates["device_type"] = *request.Updates.DeviceType
}
if request.Updates.Longitude != nil {
updates["longitude"] = *request.Updates.Longitude
}
if request.Updates.Latitude != nil {
updates["latitude"] = *request.Updates.Latitude
}
if request.Updates.PortAmount != nil {
updates["port_amount"] = *request.Updates.PortAmount
}
if request.Updates.Status != nil {
updates["status"] = *request.Updates.Status
}
if request.Updates.Province != nil {
updates["province"] = *request.Updates.Province
}
if request.Updates.City != nil {
updates["city"] = *request.Updates.City
}
if request.Updates.District != nil {
updates["district"] = *request.Updates.District
}
if request.Updates.TowerID != nil {
updates["tower_id"] = *request.Updates.TowerID
}
if request.Updates.OLTID != nil {
updates["olt_id"] = *request.Updates.OLTID
}
// Handle images
if len(deviceImages) > 0 {
imageURLs, err := helper.SaveDeviceImagesBulk(deviceImages)
if err != nil {
errors = append(errors, res.BulkDeviceError{
Index: i,
Error: "Failed to save images",
Details: err.Error(),
})
continue
}
if replaceImages {
// Replace all images
if len(imageURLs) > 0 {
updates["image_url"] = imageURLs[0]
updates["image_urls"] = entity.StringSlice(imageURLs)
}
} else {
// Append to existing images
existingImageURLs := existingDevice.GetAllImageURLs()
allImageURLs := append(existingImageURLs, imageURLs...)
if len(allImageURLs) > 0 {
updates["image_url"] = allImageURLs[0]
updates["image_urls"] = entity.StringSlice(allImageURLs)
}
}
}
// Update device
if len(updates) > 0 {
_, err = u.deviceRepo.BulkUpdate([]uuid.UUID{deviceID}, updates)
if err != nil {
errors = append(errors, res.BulkDeviceError{
Index: i,
Error: "Failed to update device",
Details: err.Error(),
})
continue
}
updatedCount++
}
// Fetch updated device
updatedDevice, err := u.deviceRepo.GetByID(deviceID)
if err == nil {
address := ""
if updatedDevice.City != nil && updatedDevice.Province != nil {
address = *updatedDevice.City + ", " + *updatedDevice.Province
} else if updatedDevice.City != nil {
address = *updatedDevice.City
} else if updatedDevice.Province != nil {
address = *updatedDevice.Province
}
responses = append(responses, res.DeviceResponse{
ID: updatedDevice.ID,
DeviceCode: updatedDevice.DeviceCode,
DeviceType: string(updatedDevice.DeviceType),
Longitude: updatedDevice.Longitude,
Latitude: updatedDevice.Latitude,
PortAmount: updatedDevice.PortAmount,
Status: string(updatedDevice.Status),
Address: address,
Region: nil,
Province: updatedDevice.Province,
City: updatedDevice.City,
District: updatedDevice.District,
ImageURL: updatedDevice.ImageURL,
ImageURLs: updatedDevice.ImageURLs,
CreatedAt: updatedDevice.CreatedAt,
UpdatedAt: updatedDevice.UpdatedAt,
})
}
}
executionTime := time.Since(startTime).String()
return res.BulkDeviceOperationResponse{
TotalRequested: len(request.DeviceIDs),
Successful: updatedCount,
Failed: len(errors),
Errors: errors,
Results: responses,
ExecutionTime: executionTime,
}, nil
}