diff --git a/delivery/controller/backbone_controller.go b/delivery/controller/backbone_controller.go index 4175ee2..833ee1e 100644 --- a/delivery/controller/backbone_controller.go +++ b/delivery/controller/backbone_controller.go @@ -1,6 +1,7 @@ package controller import ( + "log" "net/http" "users_management/m/middleware" "users_management/m/model/dto/req" @@ -54,6 +55,7 @@ func (bc *BackboneController) CreateBackbone() gin.HandlerFunc { err := c.ShouldBindJSON(&backboneDTO) if err != nil { + log.Println("Error binding JSON:", err) common.ErrorResponses(c, http.StatusBadRequest, err.Error()) return } @@ -61,6 +63,7 @@ func (bc *BackboneController) CreateBackbone() gin.HandlerFunc { err = bc.bu.CreateBackbone(backboneDTO) if err != nil { + log.Println("Error creating backbone:", err) common.ErrorResponses(c, http.StatusBadRequest, err.Error()) return } diff --git a/delivery/controller/device_details.go b/delivery/controller/device_details.go index 10b23c0..f7a928f 100644 --- a/delivery/controller/device_details.go +++ b/delivery/controller/device_details.go @@ -6,7 +6,6 @@ import ( "users_management/m/model/dto/req" "users_management/m/usecase" "users_management/m/utils/common" - "github.com/gin-gonic/gin" "github.com/google/uuid" ) diff --git a/delivery/controller/tower_controller.go b/delivery/controller/tower_controller.go index 6bbab91..61587a7 100644 --- a/delivery/controller/tower_controller.go +++ b/delivery/controller/tower_controller.go @@ -63,18 +63,22 @@ func (tc *TowerController) CreateTower() gin.HandlerFunc { towerCode := c.PostForm("tower_code") longitudeStr := c.PostForm("longitude") latitudeStr := c.PostForm("latitude") + externalTowerStr := c.PostForm("external_tower") // Validate required fields - if deviceIDStr == "" || towerCode == "" || longitudeStr == "" || latitudeStr == "" { + if towerCode == "" || longitudeStr == "" || latitudeStr == "" { common.ErrorResponses(c, http.StatusBadRequest, "Missing required fields") return } - // Parse UUID - deviceID, err := uuid.Parse(deviceIDStr) - if err != nil { - common.ErrorResponses(c, http.StatusBadRequest, "Invalid device ID") - return + var deviceID *uuid.UUID + if deviceIDStr != "" { + parsedDeviceID, err := uuid.Parse(deviceIDStr) + if err != nil { + common.ErrorResponses(c, http.StatusBadRequest, "Invalid device ID") + return + } + deviceID = &parsedDeviceID } // Parse coordinates @@ -90,12 +94,23 @@ func (tc *TowerController) CreateTower() gin.HandlerFunc { return } + var externalTower *bool + if externalTowerStr != "" { + parsedExternalTower, err := strconv.ParseBool(externalTowerStr) + if err != nil { + common.ErrorResponses(c, http.StatusBadRequest, "Invalid external_tower value") + return + } + externalTower = &parsedExternalTower + } + // Create DTO towerDTO := req.TowerDTO{ - DeviceID: deviceID, - TowerCode: towerCode, - Longitude: longitude, - Latitude: latitude, + DeviceID: deviceID, + TowerCode: towerCode, + Longitude: longitude, + Latitude: latitude, + ExternalTower: externalTower, } // Get image file (optional) diff --git a/model/dto/req/tower_dto.go b/model/dto/req/tower_dto.go index 5d7bfe5..8dab41c 100644 --- a/model/dto/req/tower_dto.go +++ b/model/dto/req/tower_dto.go @@ -3,18 +3,20 @@ package req import "github.com/google/uuid" type TowerDTO struct { - DeviceID uuid.UUID `json:"dev_id"` - DeviceName string `json:"device_name"` - TowerCode string `json:"tower_code"` - Longitude float64 `json:"longitude"` - Latitude float64 `json:"latitude"` + DeviceID *uuid.UUID `json:"dev_id,omitempty"` + DeviceName *string `json:"device_name,omitempty"` + TowerCode string `json:"tower_code" validate:"required"` + Longitude float64 `json:"longitude" validate:"required"` + Latitude float64 `json:"latitude" validate:"required"` + ExternalTower *bool `json:"external_tower,omitempty"` // Make nullable } type UpdateTowerDTO struct { - DeviceID *uuid.UUID `json:"device_id,omitempty" validate:"omitempty,min=3"` - DeviceName *string `json:"device_name,omitempty" validate:"omitempty,min=3"` - TowerCode *string `json:"tower_code,omitempty" validate:"omitempty"` - Longitude *float64 `json:"longitude,omitempty" validate:"omitempty,longitude"` - Latitude *float64 `json:"latitude,omitempty" validate:"omitempty,latitude"` - ImageURL *string `json:"image_url,omitempty" validate:"omitempty,url"` + DeviceID *uuid.UUID `json:"device_id,omitempty"` + DeviceName *string `json:"device_name,omitempty"` + TowerCode *string `json:"tower_code,omitempty"` + Longitude *float64 `json:"longitude,omitempty" validate:"omitempty,longitude"` + Latitude *float64 `json:"latitude,omitempty" validate:"omitempty,latitude"` + ExternalTower *bool `json:"external_tower,omitempty"` // Make nullable + ImageURL *string `json:"image_url,omitempty"` } \ No newline at end of file diff --git a/model/dto/res/device_details.go b/model/dto/res/device_details.go index 2a95562..47396ef 100644 --- a/model/dto/res/device_details.go +++ b/model/dto/res/device_details.go @@ -35,5 +35,6 @@ type TowerConnectionDetail struct { ID uuid.UUID `json:"id"` TowerCode string `json:"tower_code"` Distance float64 `json:"distance_km"` // Distance from tower to device + ExternalTower *bool `json:"external_tower"` // Indicates if this is an external tower ImageURL *string `json:"image_url,omitempty"` } \ No newline at end of file diff --git a/model/dto/res/tower_res.go b/model/dto/res/tower_res.go index 6344360..d97589b 100644 --- a/model/dto/res/tower_res.go +++ b/model/dto/res/tower_res.go @@ -1,18 +1,19 @@ package res import ( - "time" - - "github.com/google/uuid" + "time" + "github.com/google/uuid" ) type TowerResponse struct { - ID uuid.UUID `json:"id"` - DeviceCode string `json:"device_code"` - TowerCode *string `json:"tower_code"` - Longitude float64 `json:"longitude"` - Latitude float64 `json:"latitude"` - Address string `json:"address"` - ImageURL string `json:"image_url"` - CreatedAt time.Time `json:"created_at"` -} + ID uuid.UUID `json:"id"` + DeviceCode *string `json:"device_code,omitempty"` // Make nullable + TowerCode *string `json:"tower_code"` + Longitude float64 `json:"longitude"` + Latitude float64 `json:"latitude"` + Address string `json:"address"` + ImageURL string `json:"image_url"` + ExternalTower *bool `json:"external_tower,omitempty"` // Make nullable + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` +} \ No newline at end of file diff --git a/model/entity/device_port.go b/model/entity/device_port.go index b3ec993..ad101fc 100644 --- a/model/entity/device_port.go +++ b/model/entity/device_port.go @@ -7,14 +7,16 @@ import ( type DevicePort struct { ID uuid.UUID `json:"id" gorm:"type:uuid;primaryKey"` - DeviceID uuid.UUID `json:"device_id" gorm:"type:uuid;column:device_id;unique"` - PortUsed int `json:"port_used" gorm:"default:0"` // Auto-calculated - PortAvailable int `json:"port_available" gorm:"default:0"` // Auto-calculated + DeviceID uuid.UUID `json:"device_id" gorm:"type:uuid;not null"` + PortUsed int `json:"port_used"` + PortAvailable int `json:"port_available"` + CustomerCount int `json:"customer_count"` // Add this field + CustomerNames []string `json:"customer_names" gorm:"type:json"` // Store customer names as JSON array CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at"` - + // Relationships - Device Device `json:"device" gorm:"foreignKey:DeviceID"` + Device Device `json:"device" gorm:"foreignKey:DeviceID"` } func (DevicePort) TableName() string { diff --git a/model/entity/devices.go b/model/entity/devices.go index 11da6ad..8c45be8 100644 --- a/model/entity/devices.go +++ b/model/entity/devices.go @@ -11,6 +11,8 @@ type DeviceStatus string const ( ODP DeviceType = "ODP" OTB DeviceType = "OTB" + + Closure DeviceType = "closure" ActiveDev DeviceStatus = "active" InactiveDev DeviceStatus = "inactive" diff --git a/model/entity/tower.go b/model/entity/tower.go index 1265566..9a59dc5 100644 --- a/model/entity/tower.go +++ b/model/entity/tower.go @@ -6,16 +6,17 @@ import ( ) type Tower struct { - ID uuid.UUID `json:"id" gorm:"type:uuid;primaryKey"` - DeviceID uuid.UUID `json:"dev_id" gorm:"type:uuid;column:dev_id"` - TowerCode string `json:"tower_code" gorm:"unique"` - Longitude float64 `json:"longitude"` - Latitude float64 `json:"latitude"` - ImageURL string `json:"image_url" gorm:"column:image_url"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` + ID uuid.UUID `json:"id" gorm:"type:uuid;primaryKey"` + DeviceID *uuid.UUID `json:"dev_id,omitempty" gorm:"type:uuid;column:dev_id;null"` // Make nullable + TowerCode string `json:"tower_code" gorm:"unique"` + Longitude float64 `json:"longitude"` + Latitude float64 `json:"latitude"` + ImageURL string `json:"image_url" gorm:"column:image_url"` + ExternalTower *bool `json:"external_tower,omitempty" gorm:"column:external_tower;null"` // Make nullable and fix typo + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` - Device Device `gorm:"foreignKey:DeviceID"` + Device *Device `json:"device,omitempty" gorm:"foreignKey:DeviceID"` // Make nullable } func (Tower) TableName() string { diff --git a/repository/backbone_repo.go b/repository/backbone_repo.go index 7d98d63..2bcdf3e 100644 --- a/repository/backbone_repo.go +++ b/repository/backbone_repo.go @@ -12,6 +12,7 @@ type BackboneRepo interface { GetAll() ([]entity.Backbone, error) Update(id uuid.UUID, updates map[string]interface{}) error GetByID(id uuid.UUID) (entity.Backbone, error) + WithTransaction(fn func(*gorm.DB) error) error } type backboneRepo struct { @@ -57,4 +58,8 @@ func (r *backboneRepo) GetByID(id uuid.UUID) (entity.Backbone, error) { return backbone, err } return backbone, nil +} + +func (r *backboneRepo) WithTransaction(fn func(*gorm.DB) error) error { + return r.db.Transaction(fn) } \ No newline at end of file diff --git a/repository/device_details.go b/repository/device_details.go index 806d24a..9e82370 100644 --- a/repository/device_details.go +++ b/repository/device_details.go @@ -1,10 +1,13 @@ package repository import ( - "errors" - "users_management/m/model/entity" - "github.com/google/uuid" - "gorm.io/gorm" + "errors" + "fmt" + "time" + "users_management/m/model/entity" + + "github.com/google/uuid" + "gorm.io/gorm" ) type DeviceDetailsRepo interface { @@ -16,7 +19,7 @@ type DeviceDetailsRepo interface { // Port management UpdateDevicePortUsage(deviceID uuid.UUID) error - ValidatePortAvailability(deviceID uuid.UUID, requiredPorts int) error + // ValidatePortAvailability(deviceID uuid.UUID, requiredPorts int) error // Connection management GetBackbonesByDeviceID(deviceID uuid.UUID) ([]entity.Backbone, error) @@ -24,8 +27,9 @@ type DeviceDetailsRepo interface { GetTowersByDeviceID(deviceID uuid.UUID) ([]entity.Tower, error) // Validation helpers - CheckDeviceExists(deviceID uuid.UUID) (bool, error) GetPortUsageByDevice(deviceID uuid.UUID) (portUsed, portAvailable int, err error) + AssignCustomerToPort(deviceID uuid.UUID, customerName string) error + RemoveCustomerFromPort(deviceID uuid.UUID, customerName string) error } type deviceDetailsRepo struct { @@ -80,6 +84,7 @@ func (r *deviceDetailsRepo) GetAll() ([]entity.DeviceDetails, error) { Preload("FishbonesEnd.DeviceEnd"). Preload("FishbonesEnd.Backbone"). Preload("Towers"). + Preload("Towers.Device"). Find(&devices).Error return devices, err } @@ -103,6 +108,7 @@ func (r *deviceDetailsRepo) GetByID(id uuid.UUID) (entity.DeviceDetails, error) Preload("FishbonesEnd.DeviceEnd"). Preload("FishbonesEnd.Backbone"). Preload("Towers"). + Preload("Towers.Device"). Where("id = ?", id). First(&device).Error return device, err @@ -150,15 +156,19 @@ func (r *deviceDetailsRepo) updatePortAmountCascade(tx *gorm.DB, deviceID uuid.U func (r *deviceDetailsRepo) UpdateDevicePortUsage(deviceID uuid.UUID) error { return r.db.Transaction(func(tx *gorm.DB) error { - // Get device info + // Lock the device record to prevent race conditions var device entity.Device - if err := tx.Where("id = ?", deviceID).First(&device).Error; err != nil { + if err := tx.Set("gorm:query_option", "FOR UPDATE"). + Where("id = ?", deviceID).First(&device).Error; err != nil { return err } var portUsed int + var customerCount int + var customerNames []string - if device.DeviceType == "OTB" { + switch device.DeviceType { + case "OTB": // For OTB: count backbones (each backbone uses 1 port) var backboneCount int64 if err := tx.Model(&entity.Backbone{}). @@ -167,42 +177,103 @@ func (r *deviceDetailsRepo) UpdateDevicePortUsage(deviceID uuid.UUID) error { return err } portUsed = int(backboneCount) - } else if device.DeviceType == "ODP" { - // For ODP: sum fishbone core amounts (each core uses 1 port) + customerCount = 0 // OTB doesn't serve customers directly + + case "closure": + // For closure: count fishbones where this device is the start device + var fishboneCount int64 + if err := tx.Model(&entity.Fishbone{}). + Where("dev_start_id = ?", deviceID). + Count(&fishboneCount).Error; err != nil { + return err + } + portUsed = int(fishboneCount) + customerCount = 0 // Closure doesn't serve customers directly + + case "ODP": + // For ODP: sum fishbone core amounts where this device is the end device var totalCores int64 if err := tx.Model(&entity.Fishbone{}). - Where("dev_start_id = ? OR dev_end_id = ?", deviceID, deviceID). + Where("dev_end_id = ?", deviceID). Select("COALESCE(SUM(core_amount), 0)"). Scan(&totalCores).Error; err != nil { return err } portUsed = int(totalCores) + + // For ODP: customer count should equal port_used + // Get existing customer assignments + var existingDevicePort entity.DevicePort + if err := tx.Where("device_id = ?", deviceID).First(&existingDevicePort).Error; err == nil { + customerNames = existingDevicePort.CustomerNames + } + + // Ensure customer count matches port_used for ODP + customerCount = portUsed + + // If we have more customers than ports used, trim the list + if len(customerNames) > portUsed { + customerNames = customerNames[:portUsed] + } + + default: + portUsed = 0 + customerCount = 0 } + // Calculate port available portAvailable := device.PortAmount - portUsed - - return tx.Model(&entity.DevicePort{}). + if portAvailable < 0 { + portAvailable = 0 + } + + // Update or create DevicePort record with locking + result := tx.Set("gorm:query_option", "FOR UPDATE"). + Model(&entity.DevicePort{}). Where("device_id = ?", deviceID). Updates(map[string]interface{}{ "port_used": portUsed, "port_available": portAvailable, + "customer_count": customerCount, + "customer_names": customerNames, "updated_at": gorm.Expr("NOW()"), - }).Error + }) + + if result.Error != nil { + return result.Error + } + + // If no record was updated, create a new one + if result.RowsAffected == 0 { + devicePort := entity.DevicePort{ + ID: uuid.New(), + DeviceID: deviceID, + PortUsed: portUsed, + PortAvailable: portAvailable, + CustomerCount: customerCount, + CustomerNames: customerNames, + CreatedAt: time.Now(), + UpdatedAt: time.Now(), + } + return tx.Create(&devicePort).Error + } + + return nil }) } -func (r *deviceDetailsRepo) ValidatePortAvailability(deviceID uuid.UUID, requiredPorts int) error { - var devicePort entity.DevicePort - if err := r.db.Where("device_id = ?", deviceID).First(&devicePort).Error; err != nil { - return err - } +// func (r *deviceDetailsRepo) ValidatePortAvailability(deviceID uuid.UUID, requiredPorts int) error { +// var devicePort entity.DevicePort +// if err := r.db.Where("device_id = ?", deviceID).First(&devicePort).Error; err != nil { +// return err +// } - if devicePort.PortAvailable < requiredPorts { - return errors.New("insufficient available ports") - } +// if devicePort.Portvailable < requiredPorts { +// return errors.New("insufficient available ports") +// } - return nil -} +// return nil +// } func (r *deviceDetailsRepo) GetBackbonesByDeviceID(deviceID uuid.UUID) ([]entity.Backbone, error) { var backbones []entity.Backbone @@ -226,11 +297,7 @@ func (r *deviceDetailsRepo) GetTowersByDeviceID(deviceID uuid.UUID) ([]entity.To return towers, err } -func (r *deviceDetailsRepo) CheckDeviceExists(deviceID uuid.UUID) (bool, error) { - var count int64 - err := r.db.Model(&entity.Device{}).Where("id = ?", deviceID).Count(&count).Error - return count > 0, err -} + func (r *deviceDetailsRepo) GetPortUsageByDevice(deviceID uuid.UUID) (portUsed, portAvailable int, err error) { var devicePort entity.DevicePort @@ -251,4 +318,78 @@ func (r *deviceDetailsRepo) Delete(id uuid.UUID) error { // Delete device return tx.Delete(&entity.Device{}, id).Error }) +} + +func (r *deviceDetailsRepo) AssignCustomerToPort(deviceID uuid.UUID, customerName string) error { + return r.db.Transaction(func(tx *gorm.DB) error { + // Lock both device and device_port records + var device entity.Device + if err := tx.Set("gorm:query_option", "FOR UPDATE"). + Where("id = ?", deviceID).First(&device).Error; err != nil { + return err + } + + // Only ODP devices can have customers assigned + if device.DeviceType != "ODP" { + return fmt.Errorf("customers can only be assigned to ODP devices") + } + + var devicePort entity.DevicePort + if err := tx.Set("gorm:query_option", "FOR UPDATE"). + Where("device_id = ?", deviceID).First(&devicePort).Error; err != nil { + return fmt.Errorf("device port record not found: %w", err) + } + + // Check if there are available ports + if devicePort.PortAvailable <= 0 { + return fmt.Errorf("no available ports for customer assignment") + } + + // Check if customer is already assigned + for _, existing := range devicePort.CustomerNames { + if existing == customerName { + return fmt.Errorf("customer %s is already assigned to this device", customerName) + } + } + + // Add customer to the list + devicePort.CustomerNames = append(devicePort.CustomerNames, customerName) + devicePort.CustomerCount = len(devicePort.CustomerNames) + devicePort.PortAvailable = devicePort.PortAvailable - 1 + devicePort.UpdatedAt = time.Now() + + return tx.Save(&devicePort).Error + }) +} + +func (r *deviceDetailsRepo) RemoveCustomerFromPort(deviceID uuid.UUID, customerName string) error { + return r.db.Transaction(func(tx *gorm.DB) error { + var devicePort entity.DevicePort + if err := tx.Set("gorm:query_option", "FOR UPDATE"). + Where("device_id = ?", deviceID).First(&devicePort).Error; err != nil { + return fmt.Errorf("device port record not found: %w", err) + } + + // Find and remove customer + newCustomerNames := make([]string, 0) + found := false + for _, existing := range devicePort.CustomerNames { + if existing != customerName { + newCustomerNames = append(newCustomerNames, existing) + } else { + found = true + } + } + + if !found { + return fmt.Errorf("customer %s is not assigned to this device", customerName) + } + + devicePort.CustomerNames = newCustomerNames + devicePort.CustomerCount = len(devicePort.CustomerNames) + devicePort.PortAvailable = devicePort.PortAvailable + 1 + devicePort.UpdatedAt = time.Now() + + return tx.Save(&devicePort).Error + }) } \ No newline at end of file diff --git a/repository/devices_repo.go b/repository/devices_repo.go index 5f74164..e62d9e1 100644 --- a/repository/devices_repo.go +++ b/repository/devices_repo.go @@ -1,6 +1,7 @@ package repository import ( + "time" "users_management/m/model/entity" "github.com/google/uuid" @@ -27,13 +28,40 @@ func NewDevicesRepo(db *gorm.DB) DevicesRepo { } func (r *devicesRepo) Post(device entity.Device) error { - err := r.db.Create(&device).Error - if err != nil { - return err - } - return nil -} + return r.db.Transaction(func(tx *gorm.DB) error { + // Create the device first + if err := tx.Create(&device).Error; err != nil { + return err + } + // Create the corresponding DevicePort record + customerCount := 0 + customerNames := make([]string, 0) + + // For ODP devices, initialize customer tracking + if device.DeviceType == "ODP" { + // Customer count starts at 0, will be updated when fishbones are connected + customerCount = 0 + } + + devicePort := entity.DevicePort{ + ID: uuid.New(), + DeviceID: device.ID, + PortUsed: 0, + PortAvailable: device.PortAmount, + CustomerCount: customerCount, + CustomerNames: customerNames, + CreatedAt: time.Now(), + UpdatedAt: time.Now(), + } + + if err := tx.Create(&devicePort).Error; err != nil { + return err + } + + return nil + }) +} func (r *devicesRepo) GetAll() ([]entity.Device, error) { var devices []entity.Device err := r.db.Find(&devices).Error diff --git a/repository/fishbone_repo.go b/repository/fishbone_repo.go index bb106f2..46d0953 100644 --- a/repository/fishbone_repo.go +++ b/repository/fishbone_repo.go @@ -23,6 +23,7 @@ type FishboneRepo interface { CheckBackboneExists(id uuid.UUID) (bool, error) CheckDeviceExists(id uuid.UUID) (bool, error) Delete(id uuid.UUID) error + WithTransaction(fn func(*gorm.DB) error) error } type fishboneRepo struct { @@ -55,6 +56,10 @@ func (r *fishboneRepo) GetAll() ([]entity.Fishbone, error) { return fishbones, nil } +func (r *fishboneRepo) WithTransaction(fn func(*gorm.DB) error) error { + return r.db.Transaction(fn) +} + func (r *fishboneRepo) Update(id uuid.UUID,updates map[string]interface{}) error { err := r.db.Model(&entity.Fishbone{}).Where("id = ?", id).Updates(updates).Error if err != nil { diff --git a/repository/tower_repo.go b/repository/tower_repo.go index 117f0e4..ea798f1 100644 --- a/repository/tower_repo.go +++ b/repository/tower_repo.go @@ -12,6 +12,7 @@ type TowerRepo interface { GetAll() ([]entity.Tower, error) Update(id uuid.UUID,updates map[string]interface{}) error GetByID(id uuid.UUID) (entity.Tower, error) + CheckDeviceExists(deviceID uuid.UUID) (bool, error) } type towerRepo struct { @@ -42,6 +43,11 @@ func (r *towerRepo) GetAll() ([]entity.Tower, error) { return towers, nil } +func (r *towerRepo) CheckDeviceExists(deviceID uuid.UUID) (bool, error) { + var count int64 + err := r.db.Model(&entity.Device{}).Where("id = ?", deviceID).Count(&count).Error + return count > 0, err +} func (r *towerRepo) Update(id uuid.UUID,updates map[string]interface{}) error { err := r.db.Model(&entity.Tower{}).Where("id = ?", id).Updates(updates).Error if err != nil { diff --git a/uploads/towers/ae4227e6-8492-415f-af47-95e7464de18f_1749704734.jpg b/uploads/towers/ae4227e6-8492-415f-af47-95e7464de18f_1749704734.jpg new file mode 100644 index 0000000..ed5ce6a Binary files /dev/null and b/uploads/towers/ae4227e6-8492-415f-af47-95e7464de18f_1749704734.jpg differ diff --git a/uploads/towers/c990cf49-f6ea-4d81-97a7-d3748afec6c4_1749711025.jpeg b/uploads/towers/c990cf49-f6ea-4d81-97a7-d3748afec6c4_1749711025.jpeg new file mode 100644 index 0000000..d919e15 Binary files /dev/null and b/uploads/towers/c990cf49-f6ea-4d81-97a7-d3748afec6c4_1749711025.jpeg differ diff --git a/uploads/towers/f55d71d2-753f-4219-8650-a5f48ca09b0f_1749707561.png b/uploads/towers/f55d71d2-753f-4219-8650-a5f48ca09b0f_1749707561.png new file mode 100644 index 0000000..b5173f9 Binary files /dev/null and b/uploads/towers/f55d71d2-753f-4219-8650-a5f48ca09b0f_1749707561.png differ diff --git a/usecase/backbone_usecase.go b/usecase/backbone_usecase.go index df25410..ecfcf11 100644 --- a/usecase/backbone_usecase.go +++ b/usecase/backbone_usecase.go @@ -11,6 +11,7 @@ import ( "github.com/go-playground/validator/v10" "github.com/google/uuid" + "gorm.io/gorm" ) type BackboneUseCase interface { @@ -44,52 +45,79 @@ func (u *backboneUseCase) CreateBackbone(backbone req.BackboneDTO) error { return fmt.Errorf("validation error: %w", err) } - // Validate that both devices exist and are OTB type - startExists, err := u.deviceDetailsRepo.CheckDeviceExists(backbone.DeviceStartID) - if err != nil { - return fmt.Errorf("error checking start device: %w", err) - } - if !startExists { - return fmt.Errorf("start device does not exist") - } + return u.backboneRepo.WithTransaction(func(tx *gorm.DB) error { + // Lock device records to prevent race conditions + var startDevice entity.Device + if err := tx.Set("gorm:query_option", "FOR UPDATE"). + Where("id = ?", backbone.DeviceStartID).First(&startDevice).Error; err != nil { + return fmt.Errorf("start device not found: %w", err) + } + + var endDevice entity.Device + if err := tx.Set("gorm:query_option", "FOR UPDATE"). + Where("id = ?", backbone.DeviceEndID).First(&endDevice).Error; err != nil { + return fmt.Errorf("end device not found: %w", err) + } - endExists, err := u.deviceDetailsRepo.CheckDeviceExists(backbone.DeviceEndID) - if err != nil { - return fmt.Errorf("error checking end device: %w", err) - } - if !endExists { - return fmt.Errorf("end device does not exist") - } + // Validate device types - both devices must be OTB for backbones + if startDevice.DeviceType != "OTB" { + return fmt.Errorf("start device must be of type OTB, got %s", startDevice.DeviceType) + } + + if endDevice.DeviceType != "OTB" { + return fmt.Errorf("end device must be of type OTB, got %s", endDevice.DeviceType) + } - // Validate port availability (each backbone connection uses 1 port) - if err := u.deviceDetailsRepo.ValidatePortAvailability(backbone.DeviceStartID, 1); err != nil { - return fmt.Errorf("start device: %w", err) - } - if err := u.deviceDetailsRepo.ValidatePortAvailability(backbone.DeviceEndID, 1); err != nil { - return fmt.Errorf("end device: %w", err) - } + // Check port availability with locking + var startDevicePort entity.DevicePort + if err := tx.Set("gorm:query_option", "FOR UPDATE"). + Where("device_id = ?", backbone.DeviceStartID).First(&startDevicePort).Error; err != nil { + return fmt.Errorf("start device port record not found: %w", err) + } - newBackbone := entity.Backbone{ - ID: uuid.New(), - BackboneCode: backbone.BackboneCode, - DeviceStartID: backbone.DeviceStartID, - DeviceEndID: backbone.DeviceEndID, - CoreAmount: backbone.CoreAmount, - CreatedAt: time.Now(), - UpdatedAt: time.Now(), - } + var endDevicePort entity.DevicePort + if err := tx.Set("gorm:query_option", "FOR UPDATE"). + Where("device_id = ?", backbone.DeviceEndID).First(&endDevicePort).Error; err != nil { + return fmt.Errorf("end device port record not found: %w", err) + } - // Create backbone - err = u.backboneRepo.Post(newBackbone) - if err != nil { - return err - } + // Validate port availability - each backbone uses 1 port regardless of core amount + if startDevicePort.PortAvailable < 1 { + return fmt.Errorf("start device has no available ports (available: %d, required: 1)", + startDevicePort.PortAvailable) + } - // Update port usage for both devices - u.deviceDetailsRepo.UpdateDevicePortUsage(backbone.DeviceStartID) - u.deviceDetailsRepo.UpdateDevicePortUsage(backbone.DeviceEndID) + if endDevicePort.PortAvailable < 1 { + return fmt.Errorf("end device has no available ports (available: %d, required: 1)", + endDevicePort.PortAvailable) + } - return nil + newBackbone := entity.Backbone{ + ID: uuid.New(), + BackboneCode: backbone.BackboneCode, + DeviceStartID: backbone.DeviceStartID, + DeviceEndID: backbone.DeviceEndID, + CoreAmount: backbone.CoreAmount, + CreatedAt: time.Now(), + UpdatedAt: time.Now(), + } + + // Create backbone + if err := tx.Create(&newBackbone).Error; err != nil { + return err + } + + // Update port usage for both devices + if err := u.updateDevicePortUsageInTx(tx, backbone.DeviceStartID); err != nil { + return fmt.Errorf("failed to update start device port usage: %w", err) + } + + if err := u.updateDevicePortUsageInTx(tx, backbone.DeviceEndID); err != nil { + return fmt.Errorf("failed to update end device port usage: %w", err) + } + + return nil + }) } @@ -138,75 +166,134 @@ func (u *backboneUseCase) UpdateBackbone(id uuid.UUID, backbone req.UpdateBackbo return fmt.Errorf("validation error: %w", err) } - // Get original backbone to track changes - originalBackbone, err := u.backboneRepo.GetByID(id) - if err != nil { + return u.backboneRepo.WithTransaction(func(tx *gorm.DB) error { + // Get original backbone for comparison with lock + var originalBackbone entity.Backbone + if err := tx.Set("gorm:query_option", "FOR UPDATE"). + Where("id = ?", id).First(&originalBackbone).Error; err != nil { + if err == gorm.ErrRecordNotFound { + return fmt.Errorf("backbone not found") + } + return err + } + + updates := make(map[string]interface{}) + devicesToUpdate := make(map[uuid.UUID]bool) + devicesToUpdate[originalBackbone.DeviceStartID] = true + devicesToUpdate[originalBackbone.DeviceEndID] = true + + // Validate device type changes if devices are being changed + if backbone.DeviceStartID != nil && *backbone.DeviceStartID != originalBackbone.DeviceStartID { + var newStartDevice entity.Device + if err := tx.Set("gorm:query_option", "FOR UPDATE"). + Where("id = ?", *backbone.DeviceStartID).First(&newStartDevice).Error; err != nil { + return fmt.Errorf("new start device not found: %w", err) + } + if newStartDevice.DeviceType != "OTB" { + return fmt.Errorf("new start device must be of type OTB, got %s", newStartDevice.DeviceType) + } + updates["dev_start_id"] = *backbone.DeviceStartID + devicesToUpdate[*backbone.DeviceStartID] = true + } + + if backbone.DeviceEndID != nil && *backbone.DeviceEndID != originalBackbone.DeviceEndID { + var newEndDevice entity.Device + if err := tx.Set("gorm:query_option", "FOR UPDATE"). + Where("id = ?", *backbone.DeviceEndID).First(&newEndDevice).Error; err != nil { + return fmt.Errorf("new end device not found: %w", err) + } + if newEndDevice.DeviceType != "OTB" { + return fmt.Errorf("new end device must be of type OTB, got %s", newEndDevice.DeviceType) + } + updates["dev_end_id"] = *backbone.DeviceEndID + devicesToUpdate[*backbone.DeviceEndID] = true + } + + // Handle core amount changes + if backbone.CoreAmount != nil && *backbone.CoreAmount != originalBackbone.CoreAmount { + updates["core_amount"] = *backbone.CoreAmount + } + + if len(updates) == 0 { + return fmt.Errorf("no fields to update") + } + + updates["updated_at"] = time.Now() + + // Update backbone + if err := tx.Model(&entity.Backbone{}).Where("id = ?", id).Updates(updates).Error; err != nil { + return err + } + + // Update port usage for all affected devices + for deviceID := range devicesToUpdate { + if err := u.updateDevicePortUsageInTx(tx, deviceID); err != nil { + return fmt.Errorf("failed to update device port usage for device %s: %w", deviceID, err) + } + } + + return nil + }) +} + +func (u *backboneUseCase) updateDevicePortUsageInTx(tx *gorm.DB, deviceID uuid.UUID) error { + // Get device with lock + var device entity.Device + if err := tx.Set("gorm:query_option", "FOR UPDATE"). + Where("id = ?", deviceID).First(&device).Error; err != nil { return err } - - updates := make(map[string]interface{}) - - // Track devices that need port recalculation - devicesToUpdate := make(map[uuid.UUID]bool) - devicesToUpdate[originalBackbone.DeviceStartID] = true - devicesToUpdate[originalBackbone.DeviceEndID] = true - - if backbone.DeviceStartID != nil { - // Validate new start device exists and has available ports - exists, err := u.deviceDetailsRepo.CheckDeviceExists(*backbone.DeviceStartID) - if err != nil { - return fmt.Errorf("error checking new start device: %w", err) + + var portUsed int + var customerCount int + + switch device.DeviceType { + case "OTB": + // For OTB: count backbones (each backbone uses 1 port regardless of core amount) + var backboneCount int64 + if err := tx.Model(&entity.Backbone{}). + Where("dev_start_id = ? OR dev_end_id = ?", deviceID, deviceID). + Count(&backboneCount).Error; err != nil { + return err } - if !exists { - return fmt.Errorf("new start device does not exist") + portUsed = int(backboneCount) + customerCount = 0 // OTB doesn't serve customers directly + + case "closure": + // For closure: count fishbones where this device is the start device + var fishboneCount int64 + if err := tx.Model(&entity.Fishbone{}). + Where("dev_start_id = ?", deviceID). + Count(&fishboneCount).Error; err != nil { + return err } - - if err := u.deviceDetailsRepo.ValidatePortAvailability(*backbone.DeviceStartID, 1); err != nil { - return fmt.Errorf("new start device: %w", err) + portUsed = int(fishboneCount) + customerCount = 0 // Closure doesn't serve customers directly + + case "ODP": + // For ODP: sum fishbone core amounts where this device is the end device + var totalCores int64 + if err := tx.Model(&entity.Fishbone{}). + Where("dev_end_id = ?", deviceID). + Select("COALESCE(SUM(core_amount), 0)"). + Scan(&totalCores).Error; err != nil { + return err } - - updates["dev_start_id"] = *backbone.DeviceStartID - devicesToUpdate[*backbone.DeviceStartID] = true + portUsed = int(totalCores) + customerCount = portUsed // For ODP, customer count equals port_used + } + + portAvailable := device.PortAmount - portUsed + if portAvailable < 0 { + portAvailable = 0 } - if backbone.DeviceEndID != nil { - // Validate new end device exists and has available ports - exists, err := u.deviceDetailsRepo.CheckDeviceExists(*backbone.DeviceEndID) - if err != nil { - return fmt.Errorf("error checking new end device: %w", err) - } - if !exists { - return fmt.Errorf("new end device does not exist") - } - - if err := u.deviceDetailsRepo.ValidatePortAvailability(*backbone.DeviceEndID, 1); err != nil { - return fmt.Errorf("new end device: %w", err) - } - - updates["dev_end_id"] = *backbone.DeviceEndID - devicesToUpdate[*backbone.DeviceEndID] = true - } - - if backbone.CoreAmount != nil { - updates["core_amount"] = *backbone.CoreAmount - } - - if len(updates) == 0 { - return fmt.Errorf("no fields to update") - } - - updates["updated_at"] = time.Now() - - // Update backbone - err = u.backboneRepo.Update(id, updates) - if err != nil { - return err - } - - // Recalculate port usage for all affected devices - for deviceID := range devicesToUpdate { - u.deviceDetailsRepo.UpdateDevicePortUsage(deviceID) - } - - return nil -} \ No newline at end of file + return tx.Model(&entity.DevicePort{}). + Where("device_id = ?", deviceID). + Updates(map[string]interface{}{ + "port_used": portUsed, + "port_available": portAvailable, + "customer_count": customerCount, + "updated_at": gorm.Expr("NOW()"), + }).Error +} diff --git a/usecase/device_details.go b/usecase/device_details.go index 59d67e9..bfecb26 100644 --- a/usecase/device_details.go +++ b/usecase/device_details.go @@ -23,7 +23,7 @@ type DeviceDetailsUseCase interface { DeleteDeviceDetails(id uuid.UUID) error // Port management - ValidatePortUsage(deviceID uuid.UUID, requiredPorts int) error + // ValidatePortUsage(deviceID uuid.UUID, requiredPorts int) error RecalculatePortUsage(deviceID uuid.UUID) error } @@ -90,13 +90,6 @@ func (u *deviceDetailsUseCase) UpdateDeviceDetails(id uuid.UUID, deviceDTO req.U } // Check if device exists - exists, err := u.deviceDetailsRepo.CheckDeviceExists(id) - if err != nil { - return err - } - if !exists { - return errors.New("device not found") - } updates := map[string]interface{}{} @@ -177,9 +170,6 @@ func (u *deviceDetailsUseCase) DeleteDeviceDetails(id uuid.UUID) error { return u.deviceDetailsRepo.Delete(id) } -func (u *deviceDetailsUseCase) ValidatePortUsage(deviceID uuid.UUID, requiredPorts int) error { - return u.deviceDetailsRepo.ValidatePortAvailability(deviceID, requiredPorts) -} func (u *deviceDetailsUseCase) RecalculatePortUsage(deviceID uuid.UUID) error { return u.deviceDetailsRepo.UpdateDevicePortUsage(deviceID) diff --git a/usecase/device_usecase.go b/usecase/device_usecase.go index ff8d5ff..a400043 100644 --- a/usecase/device_usecase.go +++ b/usecase/device_usecase.go @@ -57,6 +57,7 @@ func (u *deviceUseCase) CreateDevice(device req.DeviceDTO) error { CreatedAt: time.Now(), UpdatedAt: time.Now(), } + return u.deviceRepo.Post(newDevice) } diff --git a/usecase/fishbone_usecase.go b/usecase/fishbone_usecase.go index 57db4fe..5a62fba 100644 --- a/usecase/fishbone_usecase.go +++ b/usecase/fishbone_usecase.go @@ -12,6 +12,7 @@ import ( "github.com/go-playground/validator/v10" "github.com/google/uuid" + "gorm.io/gorm" ) type FishboneUseCase interface { @@ -47,53 +48,129 @@ func (u *fishboneUseCase) CreateFishbone(fishbone req.FishboneDTO) error { return fmt.Errorf("validation error: %w", err) } - // Validate that both devices exist and are ODP type - startExists, err := u.deviceDetailsRepo.CheckDeviceExists(fishbone.DeviceStartID) - if err != nil { - return fmt.Errorf("error checking start device: %w", err) - } - if !startExists { - return fmt.Errorf("start device does not exist") - } + return u.fishboneRepo.WithTransaction(func(tx *gorm.DB) error { + // Lock device records to prevent race conditions + var startDevice entity.Device + if err := tx.Set("gorm:query_option", "FOR UPDATE"). + Where("id = ?", fishbone.DeviceStartID).First(&startDevice).Error; err != nil { + return fmt.Errorf("start device not found: %w", err) + } + + var endDevice entity.Device + if err := tx.Set("gorm:query_option", "FOR UPDATE"). + Where("id = ?", fishbone.DeviceEndID).First(&endDevice).Error; err != nil { + return fmt.Errorf("end device not found: %w", err) + } - endExists, err := u.deviceDetailsRepo.CheckDeviceExists(fishbone.DeviceEndID) - if err != nil { - return fmt.Errorf("error checking end device: %w", err) - } - if !endExists { - return fmt.Errorf("end device does not exist") - } + // Validate device types + if startDevice.DeviceType != "closure" { + return fmt.Errorf("start device must be of type closure, got %s", startDevice.DeviceType) + } + + if endDevice.DeviceType != "ODP" { + return fmt.Errorf("end device must be of type ODP, got %s", endDevice.DeviceType) + } - // Validate port availability for ODP devices (each core needs 1 port) - if err := u.deviceDetailsRepo.ValidatePortAvailability(fishbone.DeviceStartID, fishbone.CoreAmount); err != nil { - return fmt.Errorf("start device: %w", err) - } - if err := u.deviceDetailsRepo.ValidatePortAvailability(fishbone.DeviceEndID, fishbone.CoreAmount); err != nil { - return fmt.Errorf("end device: %w", err) - } + // Check port availability with locking + var startDevicePort entity.DevicePort + if err := tx.Set("gorm:query_option", "FOR UPDATE"). + Where("device_id = ?", fishbone.DeviceStartID).First(&startDevicePort).Error; err != nil { + return fmt.Errorf("start device port record not found: %w", err) + } - newFishbone := entity.Fishbone{ - ID: uuid.New(), - FishboneCode: fishbone.FishboneCode, - BackboneID: fishbone.BackboneID, - DeviceStartID: fishbone.DeviceStartID, - DeviceEndID: fishbone.DeviceEndID, - CoreAmount: fishbone.CoreAmount, - CreatedAt: time.Now(), - UpdatedAt: time.Now(), - } + var endDevicePort entity.DevicePort + if err := tx.Set("gorm:query_option", "FOR UPDATE"). + Where("device_id = ?", fishbone.DeviceEndID).First(&endDevicePort).Error; err != nil { + return fmt.Errorf("end device port record not found: %w", err) + } - // Create fishbone - err = u.fishboneRepo.Post(newFishbone) - if err != nil { + // Validate port availability + if startDevicePort.PortAvailable < 1 { + return fmt.Errorf("start device has no available ports (available: %d, required: 1)", + startDevicePort.PortAvailable) + } + + if endDevicePort.PortAvailable < fishbone.CoreAmount { + return fmt.Errorf("end device has insufficient available ports (available: %d, required: %d)", + endDevicePort.PortAvailable, fishbone.CoreAmount) + } + + newFishbone := entity.Fishbone{ + ID: uuid.New(), + FishboneCode: fishbone.FishboneCode, + BackboneID: fishbone.BackboneID, + DeviceStartID: fishbone.DeviceStartID, + DeviceEndID: fishbone.DeviceEndID, + CoreAmount: fishbone.CoreAmount, + CreatedAt: time.Now(), + UpdatedAt: time.Now(), + } + + // Create fishbone + if err := tx.Create(&newFishbone).Error; err != nil { + return err + } + + // Update port usage for both devices + if err := u.updateDevicePortUsageInTx(tx, fishbone.DeviceStartID); err != nil { + return fmt.Errorf("failed to update start device port usage: %w", err) + } + + if err := u.updateDevicePortUsageInTx(tx, fishbone.DeviceEndID); err != nil { + return fmt.Errorf("failed to update end device port usage: %w", err) + } + + return nil + }) +} + +func (u *fishboneUseCase) updateDevicePortUsageInTx(tx *gorm.DB, deviceID uuid.UUID) error { + // Get device with lock + var device entity.Device + if err := tx.Set("gorm:query_option", "FOR UPDATE"). + Where("id = ?", deviceID).First(&device).Error; err != nil { return err } + + var portUsed int + var customerCount int + + switch device.DeviceType { + case "closure": + var fishboneCount int64 + if err := tx.Model(&entity.Fishbone{}). + Where("dev_start_id = ?", deviceID). + Count(&fishboneCount).Error; err != nil { + return err + } + portUsed = int(fishboneCount) + customerCount = 0 + + case "ODP": + var totalCores int64 + if err := tx.Model(&entity.Fishbone{}). + Where("dev_end_id = ?", deviceID). + Select("COALESCE(SUM(core_amount), 0)"). + Scan(&totalCores).Error; err != nil { + return err + } + portUsed = int(totalCores) + customerCount = portUsed // For ODP, customer count equals port_used + } + + portAvailable := device.PortAmount - portUsed + if portAvailable < 0 { + portAvailable = 0 + } - // Update port usage for both devices - u.deviceDetailsRepo.UpdateDevicePortUsage(fishbone.DeviceStartID) - u.deviceDetailsRepo.UpdateDevicePortUsage(fishbone.DeviceEndID) - - return nil + return tx.Model(&entity.DevicePort{}). + Where("device_id = ?", deviceID). + Updates(map[string]interface{}{ + "port_used": portUsed, + "port_available": portAvailable, + "customer_count": customerCount, + "updated_at": gorm.Expr("NOW()"), + }).Error } func (u *fishboneUseCase) GetAllFishbone() ([]res.FishboneResponse, error) { @@ -121,106 +198,88 @@ func (u *fishboneUseCase) UpdateFishbone(id uuid.UUID, fishbone req.UpdateFishbo return fmt.Errorf("validation error: %w", err) } - // Get original fishbone for comparison - originalFishbone, err := u.fishboneRepo.GetByID(id) - if err != nil { - return err - } - - updates := make(map[string]interface{}) - - // Track devices that need port recalculation - devicesToUpdate := make(map[uuid.UUID]bool) - devicesToUpdate[originalFishbone.DeviceStartID] = true - devicesToUpdate[originalFishbone.DeviceEndID] = true - - // If core amount is changed, validate port availability - if fishbone.CoreAmount != nil && *fishbone.CoreAmount != originalFishbone.CoreAmount { - coreDiff := *fishbone.CoreAmount - originalFishbone.CoreAmount - if coreDiff > 0 { - // Increasing cores - check port availability - if err := u.deviceDetailsRepo.ValidatePortAvailability(originalFishbone.DeviceStartID, coreDiff); err != nil { - return fmt.Errorf("start device: %w", err) + return u.fishboneRepo.WithTransaction(func(tx *gorm.DB) error { + // Get original fishbone for comparison with lock + var originalFishbone entity.Fishbone + if err := tx.Set("gorm:query_option", "FOR UPDATE"). + Where("id = ?", id).First(&originalFishbone).Error; err != nil { + if err == gorm.ErrRecordNotFound { + return fmt.Errorf("fishbone not found") } - if err := u.deviceDetailsRepo.ValidatePortAvailability(originalFishbone.DeviceEndID, coreDiff); err != nil { - return fmt.Errorf("end device: %w", err) + return err + } + + updates := make(map[string]interface{}) + devicesToUpdate := make(map[uuid.UUID]bool) + devicesToUpdate[originalFishbone.DeviceStartID] = true + devicesToUpdate[originalFishbone.DeviceEndID] = true + + // Validate device type changes if devices are being changed + if fishbone.DeviceStartID != nil && *fishbone.DeviceStartID != originalFishbone.DeviceStartID { + var newStartDevice entity.Device + if err := tx.Set("gorm:query_option", "FOR UPDATE"). + Where("id = ?", *fishbone.DeviceStartID).First(&newStartDevice).Error; err != nil { + return fmt.Errorf("new start device not found: %w", err) + } + if newStartDevice.DeviceType != "closure" { + return fmt.Errorf("new start device must be of type closure, got %s", newStartDevice.DeviceType) + } + updates["dev_start_id"] = *fishbone.DeviceStartID + devicesToUpdate[*fishbone.DeviceStartID] = true + } + + if fishbone.DeviceEndID != nil && *fishbone.DeviceEndID != originalFishbone.DeviceEndID { + var newEndDevice entity.Device + if err := tx.Set("gorm:query_option", "FOR UPDATE"). + Where("id = ?", *fishbone.DeviceEndID).First(&newEndDevice).Error; err != nil { + return fmt.Errorf("new end device not found: %w", err) + } + if newEndDevice.DeviceType != "ODP" { + return fmt.Errorf("new end device must be of type ODP, got %s", newEndDevice.DeviceType) + } + updates["dev_end_id"] = *fishbone.DeviceEndID + devicesToUpdate[*fishbone.DeviceEndID] = true + } + + // Handle core amount changes + if fishbone.CoreAmount != nil && *fishbone.CoreAmount != originalFishbone.CoreAmount { + updates["core_amount"] = *fishbone.CoreAmount + } + + if fishbone.BackboneID != nil { + // Validate backbone exists + var backbone entity.Backbone + if err := tx.Where("id = ?", *fishbone.BackboneID).First(&backbone).Error; err != nil { + return fmt.Errorf("backbone not found: %w", err) + } + updates["bb_id"] = *fishbone.BackboneID + } + + if fishbone.FishboneCode != nil { + updates["fishbone_code"] = *fishbone.FishboneCode + } + + if len(updates) == 0 { + return fmt.Errorf("no fields to update") + } + + updates["updated_at"] = time.Now() + + // Update fishbone + if err := tx.Model(&entity.Fishbone{}).Where("id = ?", id).Updates(updates).Error; err != nil { + return err + } + + // Update port usage for all affected devices + for deviceID := range devicesToUpdate { + if err := u.updateDevicePortUsageInTx(tx, deviceID); err != nil { + return fmt.Errorf("failed to update device port usage for device %s: %w", deviceID, err) } } - updates["core_amount"] = *fishbone.CoreAmount - } - if fishbone.DeviceStartID != nil { - exists, err := u.deviceDetailsRepo.CheckDeviceExists(*fishbone.DeviceStartID) - if err != nil { - return fmt.Errorf("error checking new start device: %w", err) - } - if !exists { - return fmt.Errorf("new start device does not exist") - } - - coreAmount := originalFishbone.CoreAmount - if fishbone.CoreAmount != nil { - coreAmount = *fishbone.CoreAmount - } - - if err := u.deviceDetailsRepo.ValidatePortAvailability(*fishbone.DeviceStartID, coreAmount); err != nil { - return fmt.Errorf("new start device: %w", err) - } - - updates["dev_start_id"] = *fishbone.DeviceStartID - devicesToUpdate[*fishbone.DeviceStartID] = true - } - - if fishbone.DeviceEndID != nil { - exists, err := u.deviceDetailsRepo.CheckDeviceExists(*fishbone.DeviceEndID) - if err != nil { - return fmt.Errorf("error checking new end device: %w", err) - } - if !exists { - return fmt.Errorf("new end device does not exist") - } - - coreAmount := originalFishbone.CoreAmount - if fishbone.CoreAmount != nil { - coreAmount = *fishbone.CoreAmount - } - - if err := u.deviceDetailsRepo.ValidatePortAvailability(*fishbone.DeviceEndID, coreAmount); err != nil { - return fmt.Errorf("new end device: %w", err) - } - - updates["dev_end_id"] = *fishbone.DeviceEndID - devicesToUpdate[*fishbone.DeviceEndID] = true - } - - if fishbone.BackboneID != nil { - updates["backbone_id"] = *fishbone.BackboneID - } - - if fishbone.FishboneCode != nil { - updates["fishbone_code"] = *fishbone.FishboneCode - } - - if len(updates) == 0 { - return fmt.Errorf("no fields to update") - } - - updates["updated_at"] = time.Now() - - // Update fishbone - err = u.fishboneRepo.Update(id, updates) - if err != nil { - return err - } - - // Recalculate port usage for all affected devices - for deviceID := range devicesToUpdate { - u.deviceDetailsRepo.UpdateDevicePortUsage(deviceID) - } - - return nil + return nil + }) } - func (u *fishboneUseCase) DeleteFishbone(id uuid.UUID) error { // Check if fishbone exists exists, err := u.fishboneRepo.CheckFishboneExists(id) diff --git a/usecase/tower_usecase.go b/usecase/tower_usecase.go index dcf5dd0..9b822c5 100644 --- a/usecase/tower_usecase.go +++ b/usecase/tower_usecase.go @@ -2,6 +2,7 @@ package usecase import ( "errors" + "fmt" "mime/multipart" "time" "users_management/m/model/dto/req" @@ -37,31 +38,48 @@ func NewTowerUseCase(towerRepo repository.TowerRepo, geocoder service.GeocodingS } func (u *towerUsecase) Post(tower req.TowerDTO, imageFile *multipart.FileHeader) error { - err := u.validate.Struct(tower) - if err != nil { - return err - } + err := u.validate.Struct(tower) + if err != nil { + return err + } - var imageURL string - if imageFile != nil { - imageURL, err = helper.SaveTowerImage(imageFile) - if err != nil { - return err - } - } + // Validate that if it's not an external tower, DeviceID must be provided + if tower.ExternalTower != nil && !*tower.ExternalTower && tower.DeviceID == nil { + return fmt.Errorf("device_id is required for internal towers") + } - newTower := entity.Tower{ - ID: uuid.New(), - DeviceID: tower.DeviceID, - TowerCode: tower.TowerCode, - Longitude: tower.Longitude, - Latitude: tower.Latitude, - ImageURL: imageURL, - CreatedAt: time.Now(), - UpdatedAt: time.Now(), - } + // Validate that if DeviceID is provided, the device exists + if tower.DeviceID != nil { + deviceExists, err := u.towerRepo.CheckDeviceExists(*tower.DeviceID) + if err != nil { + return err + } + if !deviceExists { + return fmt.Errorf("device not found") + } + } - return u.towerRepo.Post(newTower) + var imageURL string + if imageFile != nil { + imageURL, err = helper.SaveTowerImage(imageFile) + if err != nil { + return err + } + } + + newTower := entity.Tower{ + ID: uuid.New(), + DeviceID: tower.DeviceID, // Now nullable + TowerCode: tower.TowerCode, + Longitude: tower.Longitude, + Latitude: tower.Latitude, + ImageURL: imageURL, + ExternalTower: tower.ExternalTower, // Now nullable + CreatedAt: time.Now(), + UpdatedAt: time.Now(), + } + + return u.towerRepo.Post(newTower) } func (u *towerUsecase) GetAll() ([]res.TowerResponse, error) { diff --git a/utils/helper/device_details.go b/utils/helper/device_details.go index 5f054c0..858c2ca 100644 --- a/utils/helper/device_details.go +++ b/utils/helper/device_details.go @@ -82,16 +82,29 @@ func ConvertToDeviceDetailsResponse(device entity.DeviceDetails, geocoder servic fishboneInfos = append(fishboneInfos, info) } - // Convert tower connections + // Convert tower connections - safely handle nullable ExternalTower towerInfos := make([]res.TowerConnectionDetail, 0) for _, tower := range device.Towers { distance := calculateDistance(device.Latitude, device.Longitude, tower.Latitude, tower.Longitude) + // Safely handle nullable ExternalTower field + var externalTower *bool + if tower.ExternalTower != nil { + externalTower = tower.ExternalTower + } // If tower.ExternalTower is nil, externalTower remains nil + + // Safely handle nullable ImageURL + var imageURL *string + if tower.ImageURL != "" { + imageURL = &tower.ImageURL + } + info := res.TowerConnectionDetail{ - ID: tower.ID, - TowerCode: tower.TowerCode, - Distance: distance, - ImageURL: &tower.ImageURL, + ID: tower.ID, + TowerCode: tower.TowerCode, + Distance: distance, + ExternalTower: externalTower, // This will be null if tower.ExternalTower is nil + ImageURL: imageURL, } towerInfos = append(towerInfos, info) } @@ -122,4 +135,6 @@ func ConvertToDeviceDetailsResponse(device entity.DeviceDetails, geocoder servic return response, nil } + + // ... rest of helper functions remain the same \ No newline at end of file diff --git a/utils/helper/towerHelperRes.go b/utils/helper/towerHelperRes.go index b6b9f69..ad898d3 100644 --- a/utils/helper/towerHelperRes.go +++ b/utils/helper/towerHelperRes.go @@ -8,63 +8,70 @@ import ( ) func ConvertToTowerResponses(towers []entity.Tower, geocoder service.GeocodingService) ([]res.TowerResponse, error) { - var responses []res.TowerResponse - - - for _, tower := range towers { - var address string + var responses []res.TowerResponse + + for _, tower := range towers { + var address string - if geocoder != nil { - generatedAddress, err := geocoder.GetAddressFromCoordinates(tower.Latitude, tower.Longitude) - if err != nil { - // Log specific geocoding error - log.Printf("Geocoding error for tower %s: %v", tower.TowerCode, err) - } else { - address = generatedAddress - } - } else { - log.Println("WARNING: Geocoder is nil") - } + if geocoder != nil { + generatedAddress, err := geocoder.GetAddressFromCoordinates(tower.Latitude, tower.Longitude) + if err != nil { + log.Printf("Geocoding error for tower %s: %v", tower.TowerCode, err) + } else { + address = generatedAddress + } + } - towerResp := res.TowerResponse{ - ID: tower.ID, - DeviceCode: tower.Device.DeviceCode, - TowerCode: &tower.TowerCode, - Longitude: tower.Longitude, - Latitude: tower.Latitude, - Address: address, - ImageURL: tower.ImageURL, - CreatedAt: tower.CreatedAt, - } - responses = append(responses, towerResp) - } - return responses, nil + var deviceCode *string + if tower.Device != nil { + deviceCode = &tower.Device.DeviceCode + } + + towerResp := res.TowerResponse{ + ID: tower.ID, + DeviceCode: deviceCode, // Now nullable + TowerCode: &tower.TowerCode, + Longitude: tower.Longitude, + Latitude: tower.Latitude, + Address: address, + ImageURL: tower.ImageURL, + ExternalTower: tower.ExternalTower, // Now nullable + CreatedAt: tower.CreatedAt, + UpdatedAt: tower.UpdatedAt, + } + responses = append(responses, towerResp) + } + return responses, nil } func ConvertToTowerIDResponses(tower entity.Tower, geocoder service.GeocodingService) (res.TowerResponse, error) { - var address string + var address string - if geocoder != nil { - generatedAddress, err := geocoder.GetAddressFromCoordinates(tower.Latitude, tower.Longitude) - if err != nil { - // Log specific geocoding error - log.Printf("Geocoding error for tower %s: %v", tower.TowerCode, err) - } else { - address = generatedAddress - } - } else { - log.Println("WARNING: Geocoder is nil") - } + if geocoder != nil { + generatedAddress, err := geocoder.GetAddressFromCoordinates(tower.Latitude, tower.Longitude) + if err != nil { + log.Printf("Geocoding error for tower %s: %v", tower.TowerCode, err) + } else { + address = generatedAddress + } + } - towerResp := res.TowerResponse{ - ID: tower.ID, - DeviceCode: tower.Device.DeviceCode, - TowerCode: &tower.TowerCode, - Longitude: tower.Longitude, - Latitude: tower.Latitude, - Address: address, - ImageURL: tower.ImageURL, - CreatedAt: tower.CreatedAt, - } - return towerResp, nil + var deviceCode *string + if tower.Device != nil { + deviceCode = &tower.Device.DeviceCode + } + + towerResp := res.TowerResponse{ + ID: tower.ID, + DeviceCode: deviceCode, // Now nullable + TowerCode: &tower.TowerCode, + Longitude: tower.Longitude, + Latitude: tower.Latitude, + Address: address, + ImageURL: tower.ImageURL, + ExternalTower: tower.ExternalTower, // Now nullable + CreatedAt: tower.CreatedAt, + UpdatedAt: tower.UpdatedAt, + } + return towerResp, nil } \ No newline at end of file