adding trx operations and more complex features

This commit is contained in:
areeqakbr 2025-06-12 22:41:55 +07:00
parent 32c775ab24
commit 751461bc34
24 changed files with 802 additions and 414 deletions

View File

@ -1,6 +1,7 @@
package controller package controller
import ( import (
"log"
"net/http" "net/http"
"users_management/m/middleware" "users_management/m/middleware"
"users_management/m/model/dto/req" "users_management/m/model/dto/req"
@ -54,6 +55,7 @@ func (bc *BackboneController) CreateBackbone() gin.HandlerFunc {
err := c.ShouldBindJSON(&backboneDTO) err := c.ShouldBindJSON(&backboneDTO)
if err != nil { if err != nil {
log.Println("Error binding JSON:", err)
common.ErrorResponses(c, http.StatusBadRequest, err.Error()) common.ErrorResponses(c, http.StatusBadRequest, err.Error())
return return
} }
@ -61,6 +63,7 @@ func (bc *BackboneController) CreateBackbone() gin.HandlerFunc {
err = bc.bu.CreateBackbone(backboneDTO) err = bc.bu.CreateBackbone(backboneDTO)
if err != nil { if err != nil {
log.Println("Error creating backbone:", err)
common.ErrorResponses(c, http.StatusBadRequest, err.Error()) common.ErrorResponses(c, http.StatusBadRequest, err.Error())
return return
} }

View File

@ -6,7 +6,6 @@ import (
"users_management/m/model/dto/req" "users_management/m/model/dto/req"
"users_management/m/usecase" "users_management/m/usecase"
"users_management/m/utils/common" "users_management/m/utils/common"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/google/uuid" "github.com/google/uuid"
) )

View File

@ -63,18 +63,22 @@ func (tc *TowerController) CreateTower() gin.HandlerFunc {
towerCode := c.PostForm("tower_code") towerCode := c.PostForm("tower_code")
longitudeStr := c.PostForm("longitude") longitudeStr := c.PostForm("longitude")
latitudeStr := c.PostForm("latitude") latitudeStr := c.PostForm("latitude")
externalTowerStr := c.PostForm("external_tower")
// Validate required fields // Validate required fields
if deviceIDStr == "" || towerCode == "" || longitudeStr == "" || latitudeStr == "" { if towerCode == "" || longitudeStr == "" || latitudeStr == "" {
common.ErrorResponses(c, http.StatusBadRequest, "Missing required fields") common.ErrorResponses(c, http.StatusBadRequest, "Missing required fields")
return return
} }
// Parse UUID var deviceID *uuid.UUID
deviceID, err := uuid.Parse(deviceIDStr) if deviceIDStr != "" {
if err != nil { parsedDeviceID, err := uuid.Parse(deviceIDStr)
common.ErrorResponses(c, http.StatusBadRequest, "Invalid device ID") if err != nil {
return common.ErrorResponses(c, http.StatusBadRequest, "Invalid device ID")
return
}
deviceID = &parsedDeviceID
} }
// Parse coordinates // Parse coordinates
@ -90,12 +94,23 @@ func (tc *TowerController) CreateTower() gin.HandlerFunc {
return 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 // Create DTO
towerDTO := req.TowerDTO{ towerDTO := req.TowerDTO{
DeviceID: deviceID, DeviceID: deviceID,
TowerCode: towerCode, TowerCode: towerCode,
Longitude: longitude, Longitude: longitude,
Latitude: latitude, Latitude: latitude,
ExternalTower: externalTower,
} }
// Get image file (optional) // Get image file (optional)

View File

@ -3,18 +3,20 @@ package req
import "github.com/google/uuid" import "github.com/google/uuid"
type TowerDTO struct { type TowerDTO struct {
DeviceID uuid.UUID `json:"dev_id"` DeviceID *uuid.UUID `json:"dev_id,omitempty"`
DeviceName string `json:"device_name"` DeviceName *string `json:"device_name,omitempty"`
TowerCode string `json:"tower_code"` TowerCode string `json:"tower_code" validate:"required"`
Longitude float64 `json:"longitude"` Longitude float64 `json:"longitude" validate:"required"`
Latitude float64 `json:"latitude"` Latitude float64 `json:"latitude" validate:"required"`
ExternalTower *bool `json:"external_tower,omitempty"` // Make nullable
} }
type UpdateTowerDTO struct { type UpdateTowerDTO struct {
DeviceID *uuid.UUID `json:"device_id,omitempty" validate:"omitempty,min=3"` DeviceID *uuid.UUID `json:"device_id,omitempty"`
DeviceName *string `json:"device_name,omitempty" validate:"omitempty,min=3"` DeviceName *string `json:"device_name,omitempty"`
TowerCode *string `json:"tower_code,omitempty" validate:"omitempty"` TowerCode *string `json:"tower_code,omitempty"`
Longitude *float64 `json:"longitude,omitempty" validate:"omitempty,longitude"` Longitude *float64 `json:"longitude,omitempty" validate:"omitempty,longitude"`
Latitude *float64 `json:"latitude,omitempty" validate:"omitempty,latitude"` Latitude *float64 `json:"latitude,omitempty" validate:"omitempty,latitude"`
ImageURL *string `json:"image_url,omitempty" validate:"omitempty,url"` ExternalTower *bool `json:"external_tower,omitempty"` // Make nullable
ImageURL *string `json:"image_url,omitempty"`
} }

View File

@ -35,5 +35,6 @@ type TowerConnectionDetail struct {
ID uuid.UUID `json:"id"` ID uuid.UUID `json:"id"`
TowerCode string `json:"tower_code"` TowerCode string `json:"tower_code"`
Distance float64 `json:"distance_km"` // Distance from tower to device 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"` ImageURL *string `json:"image_url,omitempty"`
} }

View File

@ -1,18 +1,19 @@
package res package res
import ( import (
"time" "time"
"github.com/google/uuid"
"github.com/google/uuid"
) )
type TowerResponse struct { type TowerResponse struct {
ID uuid.UUID `json:"id"` ID uuid.UUID `json:"id"`
DeviceCode string `json:"device_code"` DeviceCode *string `json:"device_code,omitempty"` // Make nullable
TowerCode *string `json:"tower_code"` TowerCode *string `json:"tower_code"`
Longitude float64 `json:"longitude"` Longitude float64 `json:"longitude"`
Latitude float64 `json:"latitude"` Latitude float64 `json:"latitude"`
Address string `json:"address"` Address string `json:"address"`
ImageURL string `json:"image_url"` ImageURL string `json:"image_url"`
CreatedAt time.Time `json:"created_at"` ExternalTower *bool `json:"external_tower,omitempty"` // Make nullable
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
} }

View File

@ -7,14 +7,16 @@ import (
type DevicePort struct { type DevicePort struct {
ID uuid.UUID `json:"id" gorm:"type:uuid;primaryKey"` ID uuid.UUID `json:"id" gorm:"type:uuid;primaryKey"`
DeviceID uuid.UUID `json:"device_id" gorm:"type:uuid;column:device_id;unique"` DeviceID uuid.UUID `json:"device_id" gorm:"type:uuid;not null"`
PortUsed int `json:"port_used" gorm:"default:0"` // Auto-calculated PortUsed int `json:"port_used"`
PortAvailable int `json:"port_available" gorm:"default:0"` // Auto-calculated 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"` CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"` UpdatedAt time.Time `json:"updated_at"`
// Relationships // Relationships
Device Device `json:"device" gorm:"foreignKey:DeviceID"` Device Device `json:"device" gorm:"foreignKey:DeviceID"`
} }
func (DevicePort) TableName() string { func (DevicePort) TableName() string {

View File

@ -12,6 +12,8 @@ const (
ODP DeviceType = "ODP" ODP DeviceType = "ODP"
OTB DeviceType = "OTB" OTB DeviceType = "OTB"
Closure DeviceType = "closure"
ActiveDev DeviceStatus = "active" ActiveDev DeviceStatus = "active"
InactiveDev DeviceStatus = "inactive" InactiveDev DeviceStatus = "inactive"
MaintenanceDev DeviceStatus = "maintenance" MaintenanceDev DeviceStatus = "maintenance"

View File

@ -6,16 +6,17 @@ import (
) )
type Tower struct { type Tower struct {
ID uuid.UUID `json:"id" gorm:"type:uuid;primaryKey"` ID uuid.UUID `json:"id" gorm:"type:uuid;primaryKey"`
DeviceID uuid.UUID `json:"dev_id" gorm:"type:uuid;column:dev_id"` DeviceID *uuid.UUID `json:"dev_id,omitempty" gorm:"type:uuid;column:dev_id;null"` // Make nullable
TowerCode string `json:"tower_code" gorm:"unique"` TowerCode string `json:"tower_code" gorm:"unique"`
Longitude float64 `json:"longitude"` Longitude float64 `json:"longitude"`
Latitude float64 `json:"latitude"` Latitude float64 `json:"latitude"`
ImageURL string `json:"image_url" gorm:"column:image_url"` ImageURL string `json:"image_url" gorm:"column:image_url"`
CreatedAt time.Time `json:"created_at"` ExternalTower *bool `json:"external_tower,omitempty" gorm:"column:external_tower;null"` // Make nullable and fix typo
UpdatedAt time.Time `json:"updated_at"` 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 { func (Tower) TableName() string {

View File

@ -12,6 +12,7 @@ type BackboneRepo interface {
GetAll() ([]entity.Backbone, error) GetAll() ([]entity.Backbone, error)
Update(id uuid.UUID, updates map[string]interface{}) error Update(id uuid.UUID, updates map[string]interface{}) error
GetByID(id uuid.UUID) (entity.Backbone, error) GetByID(id uuid.UUID) (entity.Backbone, error)
WithTransaction(fn func(*gorm.DB) error) error
} }
type backboneRepo struct { type backboneRepo struct {
@ -58,3 +59,7 @@ func (r *backboneRepo) GetByID(id uuid.UUID) (entity.Backbone, error) {
} }
return backbone, nil return backbone, nil
} }
func (r *backboneRepo) WithTransaction(fn func(*gorm.DB) error) error {
return r.db.Transaction(fn)
}

View File

@ -1,10 +1,13 @@
package repository package repository
import ( import (
"errors" "errors"
"users_management/m/model/entity" "fmt"
"github.com/google/uuid" "time"
"gorm.io/gorm" "users_management/m/model/entity"
"github.com/google/uuid"
"gorm.io/gorm"
) )
type DeviceDetailsRepo interface { type DeviceDetailsRepo interface {
@ -16,7 +19,7 @@ type DeviceDetailsRepo interface {
// Port management // Port management
UpdateDevicePortUsage(deviceID uuid.UUID) error UpdateDevicePortUsage(deviceID uuid.UUID) error
ValidatePortAvailability(deviceID uuid.UUID, requiredPorts int) error // ValidatePortAvailability(deviceID uuid.UUID, requiredPorts int) error
// Connection management // Connection management
GetBackbonesByDeviceID(deviceID uuid.UUID) ([]entity.Backbone, error) GetBackbonesByDeviceID(deviceID uuid.UUID) ([]entity.Backbone, error)
@ -24,8 +27,9 @@ type DeviceDetailsRepo interface {
GetTowersByDeviceID(deviceID uuid.UUID) ([]entity.Tower, error) GetTowersByDeviceID(deviceID uuid.UUID) ([]entity.Tower, error)
// Validation helpers // Validation helpers
CheckDeviceExists(deviceID uuid.UUID) (bool, error)
GetPortUsageByDevice(deviceID uuid.UUID) (portUsed, portAvailable int, err 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 { type deviceDetailsRepo struct {
@ -80,6 +84,7 @@ func (r *deviceDetailsRepo) GetAll() ([]entity.DeviceDetails, error) {
Preload("FishbonesEnd.DeviceEnd"). Preload("FishbonesEnd.DeviceEnd").
Preload("FishbonesEnd.Backbone"). Preload("FishbonesEnd.Backbone").
Preload("Towers"). Preload("Towers").
Preload("Towers.Device").
Find(&devices).Error Find(&devices).Error
return devices, err return devices, err
} }
@ -103,6 +108,7 @@ func (r *deviceDetailsRepo) GetByID(id uuid.UUID) (entity.DeviceDetails, error)
Preload("FishbonesEnd.DeviceEnd"). Preload("FishbonesEnd.DeviceEnd").
Preload("FishbonesEnd.Backbone"). Preload("FishbonesEnd.Backbone").
Preload("Towers"). Preload("Towers").
Preload("Towers.Device").
Where("id = ?", id). Where("id = ?", id).
First(&device).Error First(&device).Error
return device, err 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 { func (r *deviceDetailsRepo) UpdateDevicePortUsage(deviceID uuid.UUID) error {
return r.db.Transaction(func(tx *gorm.DB) 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 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 return err
} }
var portUsed int 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) // For OTB: count backbones (each backbone uses 1 port)
var backboneCount int64 var backboneCount int64
if err := tx.Model(&entity.Backbone{}). if err := tx.Model(&entity.Backbone{}).
@ -167,42 +177,103 @@ func (r *deviceDetailsRepo) UpdateDevicePortUsage(deviceID uuid.UUID) error {
return err return err
} }
portUsed = int(backboneCount) portUsed = int(backboneCount)
} else if device.DeviceType == "ODP" { customerCount = 0 // OTB doesn't serve customers directly
// For ODP: sum fishbone core amounts (each core uses 1 port)
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 var totalCores int64
if err := tx.Model(&entity.Fishbone{}). 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)"). Select("COALESCE(SUM(core_amount), 0)").
Scan(&totalCores).Error; err != nil { Scan(&totalCores).Error; err != nil {
return err return err
} }
portUsed = int(totalCores) 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 portAvailable := device.PortAmount - portUsed
if portAvailable < 0 {
portAvailable = 0
}
return tx.Model(&entity.DevicePort{}). // Update or create DevicePort record with locking
result := tx.Set("gorm:query_option", "FOR UPDATE").
Model(&entity.DevicePort{}).
Where("device_id = ?", deviceID). Where("device_id = ?", deviceID).
Updates(map[string]interface{}{ Updates(map[string]interface{}{
"port_used": portUsed, "port_used": portUsed,
"port_available": portAvailable, "port_available": portAvailable,
"customer_count": customerCount,
"customer_names": customerNames,
"updated_at": gorm.Expr("NOW()"), "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 { // func (r *deviceDetailsRepo) ValidatePortAvailability(deviceID uuid.UUID, requiredPorts int) error {
var devicePort entity.DevicePort // var devicePort entity.DevicePort
if err := r.db.Where("device_id = ?", deviceID).First(&devicePort).Error; err != nil { // if err := r.db.Where("device_id = ?", deviceID).First(&devicePort).Error; err != nil {
return err // return err
} // }
if devicePort.PortAvailable < requiredPorts { // if devicePort.Portvailable < requiredPorts {
return errors.New("insufficient available ports") // return errors.New("insufficient available ports")
} // }
return nil // return nil
} // }
func (r *deviceDetailsRepo) GetBackbonesByDeviceID(deviceID uuid.UUID) ([]entity.Backbone, error) { func (r *deviceDetailsRepo) GetBackbonesByDeviceID(deviceID uuid.UUID) ([]entity.Backbone, error) {
var backbones []entity.Backbone var backbones []entity.Backbone
@ -226,11 +297,7 @@ func (r *deviceDetailsRepo) GetTowersByDeviceID(deviceID uuid.UUID) ([]entity.To
return towers, err 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) { func (r *deviceDetailsRepo) GetPortUsageByDevice(deviceID uuid.UUID) (portUsed, portAvailable int, err error) {
var devicePort entity.DevicePort var devicePort entity.DevicePort
@ -252,3 +319,77 @@ func (r *deviceDetailsRepo) Delete(id uuid.UUID) error {
return tx.Delete(&entity.Device{}, id).Error 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
})
}

View File

@ -1,6 +1,7 @@
package repository package repository
import ( import (
"time"
"users_management/m/model/entity" "users_management/m/model/entity"
"github.com/google/uuid" "github.com/google/uuid"
@ -27,13 +28,40 @@ func NewDevicesRepo(db *gorm.DB) DevicesRepo {
} }
func (r *devicesRepo) Post(device entity.Device) error { func (r *devicesRepo) Post(device entity.Device) error {
err := r.db.Create(&device).Error return r.db.Transaction(func(tx *gorm.DB) error {
if err != nil { // Create the device first
return err if err := tx.Create(&device).Error; err != nil {
} return err
return nil }
}
// 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) { func (r *devicesRepo) GetAll() ([]entity.Device, error) {
var devices []entity.Device var devices []entity.Device
err := r.db.Find(&devices).Error err := r.db.Find(&devices).Error

View File

@ -23,6 +23,7 @@ type FishboneRepo interface {
CheckBackboneExists(id uuid.UUID) (bool, error) CheckBackboneExists(id uuid.UUID) (bool, error)
CheckDeviceExists(id uuid.UUID) (bool, error) CheckDeviceExists(id uuid.UUID) (bool, error)
Delete(id uuid.UUID) error Delete(id uuid.UUID) error
WithTransaction(fn func(*gorm.DB) error) error
} }
type fishboneRepo struct { type fishboneRepo struct {
@ -55,6 +56,10 @@ func (r *fishboneRepo) GetAll() ([]entity.Fishbone, error) {
return fishbones, nil 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 { func (r *fishboneRepo) Update(id uuid.UUID,updates map[string]interface{}) error {
err := r.db.Model(&entity.Fishbone{}).Where("id = ?", id).Updates(updates).Error err := r.db.Model(&entity.Fishbone{}).Where("id = ?", id).Updates(updates).Error
if err != nil { if err != nil {

View File

@ -12,6 +12,7 @@ type TowerRepo interface {
GetAll() ([]entity.Tower, error) GetAll() ([]entity.Tower, error)
Update(id uuid.UUID,updates map[string]interface{}) error Update(id uuid.UUID,updates map[string]interface{}) error
GetByID(id uuid.UUID) (entity.Tower, error) GetByID(id uuid.UUID) (entity.Tower, error)
CheckDeviceExists(deviceID uuid.UUID) (bool, error)
} }
type towerRepo struct { type towerRepo struct {
@ -42,6 +43,11 @@ func (r *towerRepo) GetAll() ([]entity.Tower, error) {
return towers, nil 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 { func (r *towerRepo) Update(id uuid.UUID,updates map[string]interface{}) error {
err := r.db.Model(&entity.Tower{}).Where("id = ?", id).Updates(updates).Error err := r.db.Model(&entity.Tower{}).Where("id = ?", id).Updates(updates).Error
if err != nil { if err != nil {

Binary file not shown.

After

Width:  |  Height:  |  Size: 85 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 283 KiB

View File

@ -11,6 +11,7 @@ import (
"github.com/go-playground/validator/v10" "github.com/go-playground/validator/v10"
"github.com/google/uuid" "github.com/google/uuid"
"gorm.io/gorm"
) )
type BackboneUseCase interface { type BackboneUseCase interface {
@ -44,52 +45,79 @@ func (u *backboneUseCase) CreateBackbone(backbone req.BackboneDTO) error {
return fmt.Errorf("validation error: %w", err) return fmt.Errorf("validation error: %w", err)
} }
// Validate that both devices exist and are OTB type return u.backboneRepo.WithTransaction(func(tx *gorm.DB) error {
startExists, err := u.deviceDetailsRepo.CheckDeviceExists(backbone.DeviceStartID) // Lock device records to prevent race conditions
if err != nil { var startDevice entity.Device
return fmt.Errorf("error checking start device: %w", err) if err := tx.Set("gorm:query_option", "FOR UPDATE").
} Where("id = ?", backbone.DeviceStartID).First(&startDevice).Error; err != nil {
if !startExists { return fmt.Errorf("start device not found: %w", err)
return fmt.Errorf("start device does not exist") }
}
endExists, err := u.deviceDetailsRepo.CheckDeviceExists(backbone.DeviceEndID) var endDevice entity.Device
if err != nil { if err := tx.Set("gorm:query_option", "FOR UPDATE").
return fmt.Errorf("error checking end device: %w", err) Where("id = ?", backbone.DeviceEndID).First(&endDevice).Error; err != nil {
} return fmt.Errorf("end device not found: %w", err)
if !endExists { }
return fmt.Errorf("end device does not exist")
}
// Validate port availability (each backbone connection uses 1 port) // Validate device types - both devices must be OTB for backbones
if err := u.deviceDetailsRepo.ValidatePortAvailability(backbone.DeviceStartID, 1); err != nil { if startDevice.DeviceType != "OTB" {
return fmt.Errorf("start device: %w", err) return fmt.Errorf("start device must be of type OTB, got %s", startDevice.DeviceType)
} }
if err := u.deviceDetailsRepo.ValidatePortAvailability(backbone.DeviceEndID, 1); err != nil {
return fmt.Errorf("end device: %w", err)
}
newBackbone := entity.Backbone{ if endDevice.DeviceType != "OTB" {
ID: uuid.New(), return fmt.Errorf("end device must be of type OTB, got %s", endDevice.DeviceType)
BackboneCode: backbone.BackboneCode, }
DeviceStartID: backbone.DeviceStartID,
DeviceEndID: backbone.DeviceEndID,
CoreAmount: backbone.CoreAmount,
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
}
// Create backbone // Check port availability with locking
err = u.backboneRepo.Post(newBackbone) var startDevicePort entity.DevicePort
if err != nil { if err := tx.Set("gorm:query_option", "FOR UPDATE").
return err Where("device_id = ?", backbone.DeviceStartID).First(&startDevicePort).Error; err != nil {
} return fmt.Errorf("start device port record not found: %w", err)
}
// Update port usage for both devices var endDevicePort entity.DevicePort
u.deviceDetailsRepo.UpdateDevicePortUsage(backbone.DeviceStartID) if err := tx.Set("gorm:query_option", "FOR UPDATE").
u.deviceDetailsRepo.UpdateDevicePortUsage(backbone.DeviceEndID) Where("device_id = ?", backbone.DeviceEndID).First(&endDevicePort).Error; err != nil {
return fmt.Errorf("end device port record not found: %w", err)
}
return nil // 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)
}
if endDevicePort.PortAvailable < 1 {
return fmt.Errorf("end device has no available ports (available: %d, required: 1)",
endDevicePort.PortAvailable)
}
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) return fmt.Errorf("validation error: %w", err)
} }
// Get original backbone to track changes return u.backboneRepo.WithTransaction(func(tx *gorm.DB) error {
originalBackbone, err := u.backboneRepo.GetByID(id) // Get original backbone for comparison with lock
if err != nil { var originalBackbone entity.Backbone
return err if err := tx.Set("gorm:query_option", "FOR UPDATE").
} Where("id = ?", id).First(&originalBackbone).Error; err != nil {
if err == gorm.ErrRecordNotFound {
updates := make(map[string]interface{}) return fmt.Errorf("backbone not found")
}
// Track devices that need port recalculation return err
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)
}
if !exists {
return fmt.Errorf("new start device does not exist")
} }
if err := u.deviceDetailsRepo.ValidatePortAvailability(*backbone.DeviceStartID, 1); err != nil { updates := make(map[string]interface{})
return fmt.Errorf("new start device: %w", err) 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
} }
updates["dev_start_id"] = *backbone.DeviceStartID if backbone.DeviceEndID != nil && *backbone.DeviceEndID != originalBackbone.DeviceEndID {
devicesToUpdate[*backbone.DeviceStartID] = true var newEndDevice entity.Device
} if err := tx.Set("gorm:query_option", "FOR UPDATE").
Where("id = ?", *backbone.DeviceEndID).First(&newEndDevice).Error; err != nil {
if backbone.DeviceEndID != nil { return fmt.Errorf("new end device not found: %w", err)
// Validate new end device exists and has available ports }
exists, err := u.deviceDetailsRepo.CheckDeviceExists(*backbone.DeviceEndID) if newEndDevice.DeviceType != "OTB" {
if err != nil { return fmt.Errorf("new end device must be of type OTB, got %s", newEndDevice.DeviceType)
return fmt.Errorf("error checking new end device: %w", err) }
} updates["dev_end_id"] = *backbone.DeviceEndID
if !exists { devicesToUpdate[*backbone.DeviceEndID] = true
return fmt.Errorf("new end device does not exist")
} }
if err := u.deviceDetailsRepo.ValidatePortAvailability(*backbone.DeviceEndID, 1); err != nil { // Handle core amount changes
return fmt.Errorf("new end device: %w", err) if backbone.CoreAmount != nil && *backbone.CoreAmount != originalBackbone.CoreAmount {
updates["core_amount"] = *backbone.CoreAmount
} }
updates["dev_end_id"] = *backbone.DeviceEndID if len(updates) == 0 {
devicesToUpdate[*backbone.DeviceEndID] = true return fmt.Errorf("no fields to update")
} }
if backbone.CoreAmount != nil { updates["updated_at"] = time.Now()
updates["core_amount"] = *backbone.CoreAmount
}
if len(updates) == 0 { // Update backbone
return fmt.Errorf("no fields to update") if err := tx.Model(&entity.Backbone{}).Where("id = ?", id).Updates(updates).Error; err != nil {
} return err
}
updates["updated_at"] = time.Now() // 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)
}
}
// Update backbone return nil
err = u.backboneRepo.Update(id, updates) })
if err != nil { }
return err
} func (u *backboneUseCase) updateDevicePortUsageInTx(tx *gorm.DB, deviceID uuid.UUID) error {
// Get device with lock
// Recalculate port usage for all affected devices var device entity.Device
for deviceID := range devicesToUpdate { if err := tx.Set("gorm:query_option", "FOR UPDATE").
u.deviceDetailsRepo.UpdateDevicePortUsage(deviceID) Where("id = ?", deviceID).First(&device).Error; err != nil {
} return err
}
return nil
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
}
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
}
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
}
portUsed = int(totalCores)
customerCount = portUsed // For ODP, customer count equals port_used
}
portAvailable := device.PortAmount - portUsed
if portAvailable < 0 {
portAvailable = 0
}
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
} }

View File

@ -23,7 +23,7 @@ type DeviceDetailsUseCase interface {
DeleteDeviceDetails(id uuid.UUID) error DeleteDeviceDetails(id uuid.UUID) error
// Port management // Port management
ValidatePortUsage(deviceID uuid.UUID, requiredPorts int) error // ValidatePortUsage(deviceID uuid.UUID, requiredPorts int) error
RecalculatePortUsage(deviceID uuid.UUID) error RecalculatePortUsage(deviceID uuid.UUID) error
} }
@ -90,13 +90,6 @@ func (u *deviceDetailsUseCase) UpdateDeviceDetails(id uuid.UUID, deviceDTO req.U
} }
// Check if device exists // 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{}{} updates := map[string]interface{}{}
@ -177,9 +170,6 @@ func (u *deviceDetailsUseCase) DeleteDeviceDetails(id uuid.UUID) error {
return u.deviceDetailsRepo.Delete(id) 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 { func (u *deviceDetailsUseCase) RecalculatePortUsage(deviceID uuid.UUID) error {
return u.deviceDetailsRepo.UpdateDevicePortUsage(deviceID) return u.deviceDetailsRepo.UpdateDevicePortUsage(deviceID)

View File

@ -58,6 +58,7 @@ func (u *deviceUseCase) CreateDevice(device req.DeviceDTO) error {
UpdatedAt: time.Now(), UpdatedAt: time.Now(),
} }
return u.deviceRepo.Post(newDevice) return u.deviceRepo.Post(newDevice)
} }

View File

@ -12,6 +12,7 @@ import (
"github.com/go-playground/validator/v10" "github.com/go-playground/validator/v10"
"github.com/google/uuid" "github.com/google/uuid"
"gorm.io/gorm"
) )
type FishboneUseCase interface { type FishboneUseCase interface {
@ -47,53 +48,129 @@ func (u *fishboneUseCase) CreateFishbone(fishbone req.FishboneDTO) error {
return fmt.Errorf("validation error: %w", err) return fmt.Errorf("validation error: %w", err)
} }
// Validate that both devices exist and are ODP type return u.fishboneRepo.WithTransaction(func(tx *gorm.DB) error {
startExists, err := u.deviceDetailsRepo.CheckDeviceExists(fishbone.DeviceStartID) // Lock device records to prevent race conditions
if err != nil { var startDevice entity.Device
return fmt.Errorf("error checking start device: %w", err) if err := tx.Set("gorm:query_option", "FOR UPDATE").
} Where("id = ?", fishbone.DeviceStartID).First(&startDevice).Error; err != nil {
if !startExists { return fmt.Errorf("start device not found: %w", err)
return fmt.Errorf("start device does not exist") }
}
endExists, err := u.deviceDetailsRepo.CheckDeviceExists(fishbone.DeviceEndID) var endDevice entity.Device
if err != nil { if err := tx.Set("gorm:query_option", "FOR UPDATE").
return fmt.Errorf("error checking end device: %w", err) Where("id = ?", fishbone.DeviceEndID).First(&endDevice).Error; err != nil {
} return fmt.Errorf("end device not found: %w", err)
if !endExists { }
return fmt.Errorf("end device does not exist")
}
// Validate port availability for ODP devices (each core needs 1 port) // Validate device types
if err := u.deviceDetailsRepo.ValidatePortAvailability(fishbone.DeviceStartID, fishbone.CoreAmount); err != nil { if startDevice.DeviceType != "closure" {
return fmt.Errorf("start device: %w", err) return fmt.Errorf("start device must be of type closure, got %s", startDevice.DeviceType)
} }
if err := u.deviceDetailsRepo.ValidatePortAvailability(fishbone.DeviceEndID, fishbone.CoreAmount); err != nil {
return fmt.Errorf("end device: %w", err)
}
newFishbone := entity.Fishbone{ if endDevice.DeviceType != "ODP" {
ID: uuid.New(), return fmt.Errorf("end device must be of type ODP, got %s", endDevice.DeviceType)
FishboneCode: fishbone.FishboneCode, }
BackboneID: fishbone.BackboneID,
DeviceStartID: fishbone.DeviceStartID,
DeviceEndID: fishbone.DeviceEndID,
CoreAmount: fishbone.CoreAmount,
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
}
// Create fishbone // Check port availability with locking
err = u.fishboneRepo.Post(newFishbone) var startDevicePort entity.DevicePort
if err != nil { 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)
}
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)
}
// 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 return err
} }
// Update port usage for both devices var portUsed int
u.deviceDetailsRepo.UpdateDevicePortUsage(fishbone.DeviceStartID) var customerCount int
u.deviceDetailsRepo.UpdateDevicePortUsage(fishbone.DeviceEndID)
return nil 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
}
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) { 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) return fmt.Errorf("validation error: %w", err)
} }
// Get original fishbone for comparison return u.fishboneRepo.WithTransaction(func(tx *gorm.DB) error {
originalFishbone, err := u.fishboneRepo.GetByID(id) // Get original fishbone for comparison with lock
if err != nil { var originalFishbone entity.Fishbone
return err if err := tx.Set("gorm:query_option", "FOR UPDATE").
} Where("id = ?", id).First(&originalFishbone).Error; err != nil {
if err == gorm.ErrRecordNotFound {
updates := make(map[string]interface{}) return fmt.Errorf("fishbone not found")
// 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)
} }
if err := u.deviceDetailsRepo.ValidatePortAvailability(originalFishbone.DeviceEndID, coreDiff); err != nil { return err
return fmt.Errorf("end device: %w", 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 { return 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
} }
func (u *fishboneUseCase) DeleteFishbone(id uuid.UUID) error { func (u *fishboneUseCase) DeleteFishbone(id uuid.UUID) error {
// Check if fishbone exists // Check if fishbone exists
exists, err := u.fishboneRepo.CheckFishboneExists(id) exists, err := u.fishboneRepo.CheckFishboneExists(id)

View File

@ -2,6 +2,7 @@ package usecase
import ( import (
"errors" "errors"
"fmt"
"mime/multipart" "mime/multipart"
"time" "time"
"users_management/m/model/dto/req" "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 { func (u *towerUsecase) Post(tower req.TowerDTO, imageFile *multipart.FileHeader) error {
err := u.validate.Struct(tower) err := u.validate.Struct(tower)
if err != nil { if err != nil {
return err return err
} }
var imageURL string // Validate that if it's not an external tower, DeviceID must be provided
if imageFile != nil { if tower.ExternalTower != nil && !*tower.ExternalTower && tower.DeviceID == nil {
imageURL, err = helper.SaveTowerImage(imageFile) return fmt.Errorf("device_id is required for internal towers")
if err != nil { }
return err
}
}
newTower := entity.Tower{ // Validate that if DeviceID is provided, the device exists
ID: uuid.New(), if tower.DeviceID != nil {
DeviceID: tower.DeviceID, deviceExists, err := u.towerRepo.CheckDeviceExists(*tower.DeviceID)
TowerCode: tower.TowerCode, if err != nil {
Longitude: tower.Longitude, return err
Latitude: tower.Latitude, }
ImageURL: imageURL, if !deviceExists {
CreatedAt: time.Now(), return fmt.Errorf("device not found")
UpdatedAt: time.Now(), }
} }
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) { func (u *towerUsecase) GetAll() ([]res.TowerResponse, error) {

View File

@ -82,16 +82,29 @@ func ConvertToDeviceDetailsResponse(device entity.DeviceDetails, geocoder servic
fishboneInfos = append(fishboneInfos, info) fishboneInfos = append(fishboneInfos, info)
} }
// Convert tower connections // Convert tower connections - safely handle nullable ExternalTower
towerInfos := make([]res.TowerConnectionDetail, 0) towerInfos := make([]res.TowerConnectionDetail, 0)
for _, tower := range device.Towers { for _, tower := range device.Towers {
distance := calculateDistance(device.Latitude, device.Longitude, tower.Latitude, tower.Longitude) 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{ info := res.TowerConnectionDetail{
ID: tower.ID, ID: tower.ID,
TowerCode: tower.TowerCode, TowerCode: tower.TowerCode,
Distance: distance, Distance: distance,
ImageURL: &tower.ImageURL, ExternalTower: externalTower, // This will be null if tower.ExternalTower is nil
ImageURL: imageURL,
} }
towerInfos = append(towerInfos, info) towerInfos = append(towerInfos, info)
} }
@ -122,4 +135,6 @@ func ConvertToDeviceDetailsResponse(device entity.DeviceDetails, geocoder servic
return response, nil return response, nil
} }
// ... rest of helper functions remain the same // ... rest of helper functions remain the same

View File

@ -8,63 +8,70 @@ import (
) )
func ConvertToTowerResponses(towers []entity.Tower, geocoder service.GeocodingService) ([]res.TowerResponse, error) { func ConvertToTowerResponses(towers []entity.Tower, geocoder service.GeocodingService) ([]res.TowerResponse, error) {
var responses []res.TowerResponse var responses []res.TowerResponse
for _, tower := range towers {
var address string
for _, tower := range towers { if geocoder != nil {
var address string 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
}
}
if geocoder != nil { var deviceCode *string
generatedAddress, err := geocoder.GetAddressFromCoordinates(tower.Latitude, tower.Longitude) if tower.Device != nil {
if err != nil { deviceCode = &tower.Device.DeviceCode
// 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")
}
towerResp := res.TowerResponse{ towerResp := res.TowerResponse{
ID: tower.ID, ID: tower.ID,
DeviceCode: tower.Device.DeviceCode, DeviceCode: deviceCode, // Now nullable
TowerCode: &tower.TowerCode, TowerCode: &tower.TowerCode,
Longitude: tower.Longitude, Longitude: tower.Longitude,
Latitude: tower.Latitude, Latitude: tower.Latitude,
Address: address, Address: address,
ImageURL: tower.ImageURL, ImageURL: tower.ImageURL,
CreatedAt: tower.CreatedAt, ExternalTower: tower.ExternalTower, // Now nullable
} CreatedAt: tower.CreatedAt,
responses = append(responses, towerResp) UpdatedAt: tower.UpdatedAt,
} }
return responses, nil responses = append(responses, towerResp)
}
return responses, nil
} }
func ConvertToTowerIDResponses(tower entity.Tower, geocoder service.GeocodingService) (res.TowerResponse, error) { func ConvertToTowerIDResponses(tower entity.Tower, geocoder service.GeocodingService) (res.TowerResponse, error) {
var address string var address string
if geocoder != nil { if geocoder != nil {
generatedAddress, err := geocoder.GetAddressFromCoordinates(tower.Latitude, tower.Longitude) generatedAddress, err := geocoder.GetAddressFromCoordinates(tower.Latitude, tower.Longitude)
if err != nil { if err != nil {
// Log specific geocoding error log.Printf("Geocoding error for tower %s: %v", tower.TowerCode, err)
log.Printf("Geocoding error for tower %s: %v", tower.TowerCode, err) } else {
} else { address = generatedAddress
address = generatedAddress }
} }
} else {
log.Println("WARNING: Geocoder is nil")
}
towerResp := res.TowerResponse{ var deviceCode *string
ID: tower.ID, if tower.Device != nil {
DeviceCode: tower.Device.DeviceCode, deviceCode = &tower.Device.DeviceCode
TowerCode: &tower.TowerCode, }
Longitude: tower.Longitude,
Latitude: tower.Latitude, towerResp := res.TowerResponse{
Address: address, ID: tower.ID,
ImageURL: tower.ImageURL, DeviceCode: deviceCode, // Now nullable
CreatedAt: tower.CreatedAt, TowerCode: &tower.TowerCode,
} Longitude: tower.Longitude,
return towerResp, nil Latitude: tower.Latitude,
Address: address,
ImageURL: tower.ImageURL,
ExternalTower: tower.ExternalTower, // Now nullable
CreatedAt: tower.CreatedAt,
UpdatedAt: tower.UpdatedAt,
}
return towerResp, nil
} }