adding trx operations and more complex features
This commit is contained in:
parent
32c775ab24
commit
751461bc34
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
)
|
||||
|
|
|
|||
|
|
@ -63,19 +63,23 @@ 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)
|
||||
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
|
||||
longitude, err := strconv.ParseFloat(longitudeStr, 64)
|
||||
|
|
@ -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,
|
||||
ExternalTower: externalTower,
|
||||
}
|
||||
|
||||
// Get image file (optional)
|
||||
|
|
|
|||
|
|
@ -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"`
|
||||
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"`
|
||||
ImageURL *string `json:"image_url,omitempty" validate:"omitempty,url"`
|
||||
ExternalTower *bool `json:"external_tower,omitempty"` // Make nullable
|
||||
ImageURL *string `json:"image_url,omitempty"`
|
||||
}
|
||||
|
|
@ -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"`
|
||||
}
|
||||
|
|
@ -2,17 +2,18 @@ package res
|
|||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type TowerResponse struct {
|
||||
ID uuid.UUID `json:"id"`
|
||||
DeviceCode string `json:"device_code"`
|
||||
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"`
|
||||
}
|
||||
|
|
@ -7,9 +7,11 @@ 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"`
|
||||
|
||||
|
|
|
|||
|
|
@ -12,6 +12,8 @@ const (
|
|||
ODP DeviceType = "ODP"
|
||||
OTB DeviceType = "OTB"
|
||||
|
||||
Closure DeviceType = "closure"
|
||||
|
||||
ActiveDev DeviceStatus = "active"
|
||||
InactiveDev DeviceStatus = "inactive"
|
||||
MaintenanceDev DeviceStatus = "maintenance"
|
||||
|
|
|
|||
|
|
@ -7,15 +7,16 @@ 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"`
|
||||
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 {
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
@ -58,3 +59,7 @@ func (r *backboneRepo) GetByID(id uuid.UUID) (entity.Backbone, error) {
|
|||
}
|
||||
return backbone, nil
|
||||
}
|
||||
|
||||
func (r *backboneRepo) WithTransaction(fn func(*gorm.DB) error) error {
|
||||
return r.db.Transaction(fn)
|
||||
}
|
||||
|
|
@ -2,7 +2,10 @@ package repository
|
|||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
"users_management/m/model/entity"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
|
@ -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,43 +177,104 @@ 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
|
||||
}
|
||||
|
||||
portAvailable := device.PortAmount - portUsed
|
||||
// Ensure customer count matches port_used for ODP
|
||||
customerCount = portUsed
|
||||
|
||||
return tx.Model(&entity.DevicePort{}).
|
||||
// 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
|
||||
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
|
||||
})
|
||||
}
|
||||
|
||||
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 result.Error != nil {
|
||||
return result.Error
|
||||
}
|
||||
|
||||
if devicePort.PortAvailable < requiredPorts {
|
||||
return errors.New("insufficient available ports")
|
||||
// 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
|
||||
// }
|
||||
|
||||
// if devicePort.Portvailable < requiredPorts {
|
||||
// return errors.New("insufficient available ports")
|
||||
// }
|
||||
|
||||
// return nil
|
||||
// }
|
||||
|
||||
func (r *deviceDetailsRepo) GetBackbonesByDeviceID(deviceID uuid.UUID) ([]entity.Backbone, error) {
|
||||
var backbones []entity.Backbone
|
||||
err := r.db.Preload("DeviceStart").Preload("DeviceEnd").
|
||||
|
|
@ -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
|
||||
|
|
@ -252,3 +319,77 @@ func (r *deviceDetailsRepo) Delete(id uuid.UUID) 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
|
||||
})
|
||||
}
|
||||
|
|
@ -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 r.db.Transaction(func(tx *gorm.DB) error {
|
||||
// Create the device first
|
||||
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) {
|
||||
var devices []entity.Device
|
||||
err := r.db.Find(&devices).Error
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
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 |
|
|
@ -11,6 +11,7 @@ import (
|
|||
|
||||
"github.com/go-playground/validator/v10"
|
||||
"github.com/google/uuid"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type BackboneUseCase interface {
|
||||
|
|
@ -44,29 +45,51 @@ 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)
|
||||
}
|
||||
|
||||
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")
|
||||
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)
|
||||
}
|
||||
|
||||
// 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)
|
||||
// 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 err := u.deviceDetailsRepo.ValidatePortAvailability(backbone.DeviceEndID, 1); err != nil {
|
||||
return fmt.Errorf("end device: %w", err)
|
||||
|
||||
if endDevice.DeviceType != "OTB" {
|
||||
return fmt.Errorf("end device must be of type OTB, got %s", endDevice.DeviceType)
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
// 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{
|
||||
|
|
@ -80,16 +103,21 @@ func (u *backboneUseCase) CreateBackbone(backbone req.BackboneDTO) error {
|
|||
}
|
||||
|
||||
// Create backbone
|
||||
err = u.backboneRepo.Post(newBackbone)
|
||||
if err != nil {
|
||||
if err := tx.Create(&newBackbone).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Update port usage for both devices
|
||||
u.deviceDetailsRepo.UpdateDevicePortUsage(backbone.DeviceStartID)
|
||||
u.deviceDetailsRepo.UpdateDevicePortUsage(backbone.DeviceEndID)
|
||||
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,56 +166,51 @@ 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{})
|
||||
|
||||
// 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)
|
||||
// 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 !exists {
|
||||
return fmt.Errorf("new start device does not exist")
|
||||
if newStartDevice.DeviceType != "OTB" {
|
||||
return fmt.Errorf("new start device must be of type OTB, got %s", newStartDevice.DeviceType)
|
||||
}
|
||||
|
||||
if err := u.deviceDetailsRepo.ValidatePortAvailability(*backbone.DeviceStartID, 1); err != nil {
|
||||
return fmt.Errorf("new start device: %w", err)
|
||||
}
|
||||
|
||||
updates["dev_start_id"] = *backbone.DeviceStartID
|
||||
devicesToUpdate[*backbone.DeviceStartID] = true
|
||||
}
|
||||
|
||||
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 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 !exists {
|
||||
return fmt.Errorf("new end device does not exist")
|
||||
if newEndDevice.DeviceType != "OTB" {
|
||||
return fmt.Errorf("new end device must be of type OTB, got %s", newEndDevice.DeviceType)
|
||||
}
|
||||
|
||||
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 {
|
||||
// Handle core amount changes
|
||||
if backbone.CoreAmount != nil && *backbone.CoreAmount != originalBackbone.CoreAmount {
|
||||
updates["core_amount"] = *backbone.CoreAmount
|
||||
}
|
||||
|
||||
|
|
@ -198,15 +221,79 @@ func (u *backboneUseCase) UpdateBackbone(id uuid.UUID, backbone req.UpdateBackbo
|
|||
updates["updated_at"] = time.Now()
|
||||
|
||||
// Update backbone
|
||||
err = u.backboneRepo.Update(id, updates)
|
||||
if err != nil {
|
||||
if err := tx.Model(&entity.Backbone{}).Where("id = ?", id).Updates(updates).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Recalculate port usage for all affected devices
|
||||
// Update port usage for all affected devices
|
||||
for deviceID := range devicesToUpdate {
|
||||
u.deviceDetailsRepo.UpdateDevicePortUsage(deviceID)
|
||||
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
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -58,6 +58,7 @@ func (u *deviceUseCase) CreateDevice(device req.DeviceDTO) error {
|
|||
UpdatedAt: time.Now(),
|
||||
}
|
||||
|
||||
|
||||
return u.deviceRepo.Post(newDevice)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ import (
|
|||
|
||||
"github.com/go-playground/validator/v10"
|
||||
"github.com/google/uuid"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type FishboneUseCase interface {
|
||||
|
|
@ -47,29 +48,51 @@ 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)
|
||||
}
|
||||
|
||||
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")
|
||||
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)
|
||||
}
|
||||
|
||||
// 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)
|
||||
// Validate device types
|
||||
if startDevice.DeviceType != "closure" {
|
||||
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)
|
||||
|
||||
if endDevice.DeviceType != "ODP" {
|
||||
return fmt.Errorf("end device must be of type ODP, got %s", endDevice.DeviceType)
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
|
||||
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{
|
||||
|
|
@ -84,16 +107,70 @@ func (u *fishboneUseCase) CreateFishbone(fishbone req.FishboneDTO) error {
|
|||
}
|
||||
|
||||
// Create fishbone
|
||||
err = u.fishboneRepo.Post(newFishbone)
|
||||
if err != nil {
|
||||
if err := tx.Create(&newFishbone).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Update port usage for both devices
|
||||
u.deviceDetailsRepo.UpdateDevicePortUsage(fishbone.DeviceStartID)
|
||||
u.deviceDetailsRepo.UpdateDevicePortUsage(fishbone.DeviceEndID)
|
||||
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
|
||||
}
|
||||
|
||||
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,80 +198,61 @@ 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 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")
|
||||
}
|
||||
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)
|
||||
// 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 err := u.deviceDetailsRepo.ValidatePortAvailability(originalFishbone.DeviceEndID, coreDiff); err != nil {
|
||||
return fmt.Errorf("end device: %w", err)
|
||||
if newStartDevice.DeviceType != "closure" {
|
||||
return fmt.Errorf("new start device must be of type closure, got %s", newStartDevice.DeviceType)
|
||||
}
|
||||
}
|
||||
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 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 !exists {
|
||||
return fmt.Errorf("new end device does not exist")
|
||||
if newEndDevice.DeviceType != "ODP" {
|
||||
return fmt.Errorf("new end device must be of type ODP, got %s", newEndDevice.DeviceType)
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
// Handle core amount changes
|
||||
if fishbone.CoreAmount != nil && *fishbone.CoreAmount != originalFishbone.CoreAmount {
|
||||
updates["core_amount"] = *fishbone.CoreAmount
|
||||
}
|
||||
|
||||
if fishbone.BackboneID != nil {
|
||||
updates["backbone_id"] = *fishbone.BackboneID
|
||||
// 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 {
|
||||
|
|
@ -208,19 +266,20 @@ func (u *fishboneUseCase) UpdateFishbone(id uuid.UUID, fishbone req.UpdateFishbo
|
|||
updates["updated_at"] = time.Now()
|
||||
|
||||
// Update fishbone
|
||||
err = u.fishboneRepo.Update(id, updates)
|
||||
if err != nil {
|
||||
if err := tx.Model(&entity.Fishbone{}).Where("id = ?", id).Updates(updates).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Recalculate port usage for all affected devices
|
||||
// Update port usage for all affected devices
|
||||
for deviceID := range devicesToUpdate {
|
||||
u.deviceDetailsRepo.UpdateDevicePortUsage(deviceID)
|
||||
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 *fishboneUseCase) DeleteFishbone(id uuid.UUID) error {
|
||||
// Check if fishbone exists
|
||||
exists, err := u.fishboneRepo.CheckFishboneExists(id)
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ package usecase
|
|||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"mime/multipart"
|
||||
"time"
|
||||
"users_management/m/model/dto/req"
|
||||
|
|
@ -42,6 +43,22 @@ func (u *towerUsecase) Post(tower req.TowerDTO, imageFile *multipart.FileHeader)
|
|||
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")
|
||||
}
|
||||
|
||||
// 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")
|
||||
}
|
||||
}
|
||||
|
||||
var imageURL string
|
||||
if imageFile != nil {
|
||||
imageURL, err = helper.SaveTowerImage(imageFile)
|
||||
|
|
@ -52,11 +69,12 @@ func (u *towerUsecase) Post(tower req.TowerDTO, imageFile *multipart.FileHeader)
|
|||
|
||||
newTower := entity.Tower{
|
||||
ID: uuid.New(),
|
||||
DeviceID: tower.DeviceID,
|
||||
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(),
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
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
|
||||
|
|
@ -10,31 +10,34 @@ import (
|
|||
func ConvertToTowerResponses(towers []entity.Tower, geocoder service.GeocodingService) ([]res.TowerResponse, error) {
|
||||
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")
|
||||
}
|
||||
|
||||
var deviceCode *string
|
||||
if tower.Device != nil {
|
||||
deviceCode = &tower.Device.DeviceCode
|
||||
}
|
||||
|
||||
towerResp := res.TowerResponse{
|
||||
ID: tower.ID,
|
||||
DeviceCode: tower.Device.DeviceCode,
|
||||
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)
|
||||
}
|
||||
|
|
@ -47,24 +50,28 @@ func ConvertToTowerIDResponses(tower entity.Tower, geocoder service.GeocodingSer
|
|||
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")
|
||||
}
|
||||
|
||||
var deviceCode *string
|
||||
if tower.Device != nil {
|
||||
deviceCode = &tower.Device.DeviceCode
|
||||
}
|
||||
|
||||
towerResp := res.TowerResponse{
|
||||
ID: tower.ID,
|
||||
DeviceCode: tower.Device.DeviceCode,
|
||||
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
|
||||
}
|
||||
Loading…
Reference in New Issue