package usecase import ( "fmt" "time" "users_management/m/model/dto/req" "users_management/m/model/dto/res" "users_management/m/model/entity" "users_management/m/repository" "users_management/m/utils/helper" "github.com/go-playground/validator/v10" "github.com/google/uuid" "gorm.io/gorm" ) type BackboneUseCase interface { CreateBackbone(backbone req.BackboneDTO) error GetAllBackbone() ([]res.BackboneResponse, error) GetByID(id uuid.UUID) (res.BackboneResponse, error) UpdateBackbone(id uuid.UUID, backbone req.UpdateBackboneDTO) error } type backboneUseCase struct { backboneRepo repository.BackboneRepo fishboneRepo repository.FishboneRepo validate *validator.Validate deviceDetailsRepo repository.DeviceDetailsRepo // Add this field } func NewBackboneUseCase(backboneRepo repository.BackboneRepo, fishboneRepo repository.FishboneRepo, deviceDetailsRepo repository.DeviceDetailsRepo) BackboneUseCase { return &backboneUseCase{ backboneRepo: backboneRepo, fishboneRepo: fishboneRepo, deviceDetailsRepo: deviceDetailsRepo, // Initialize the field validate: validator.New(), } } func (u *backboneUseCase) CreateBackbone(backbone req.BackboneDTO) error { err := u.validate.Struct(backbone) if err != nil { return fmt.Errorf("validation error: %w", err) } return u.backboneRepo.WithTransaction(func(tx *gorm.DB) error { // Lock device records to prevent race conditions var startDevice entity.Device if err := tx.Set("gorm:query_option", "FOR UPDATE"). Where("id = ?", backbone.DeviceStartID).First(&startDevice).Error; err != nil { return fmt.Errorf("start device not found: %w", err) } var endDevice entity.Device if err := tx.Set("gorm:query_option", "FOR UPDATE"). Where("id = ?", backbone.DeviceEndID).First(&endDevice).Error; err != nil { return fmt.Errorf("end device not found: %w", err) } // Validate device types - both devices must be OTB for backbones if startDevice.DeviceType != "OTB" { return fmt.Errorf("start device must be of type OTB, got %s", startDevice.DeviceType) } if endDevice.DeviceType != "OTB" { return fmt.Errorf("end device must be of type OTB, got %s", endDevice.DeviceType) } // 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 && startDevice.DeviceType == "OTB" { 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 }) } func (u *backboneUseCase) GetAllBackbone() ([]res.BackboneResponse, error) { backbones, err := u.backboneRepo.GetAll() if err != nil { return nil, err } totalFishbone,err := u.fishboneRepo.CountFishbone() if err != nil { return nil, err } backboneResp, err := helper.ConvertToBackboneResponses(backbones,totalFishbone) if err != nil { return nil, err } return backboneResp, nil } func (u *backboneUseCase) GetByID(id uuid.UUID) (res.BackboneResponse, error) { backbone, err := u.backboneRepo.GetByID(id) if err != nil { return res.BackboneResponse{}, err } fishboneCount, err := u.fishboneRepo.CountFishboneByBackboneID(backbone.ID) if err != nil { return res.BackboneResponse{}, err } backboneResp, err := helper.ConvertToBackboneRespId(backbone,fishboneCount) if err != nil { return res.BackboneResponse{}, err } return backboneResp, nil } func (u *backboneUseCase) UpdateBackbone(id uuid.UUID, backbone req.UpdateBackboneDTO) error { err := u.validate.Struct(backbone) if err != nil { return fmt.Errorf("validation error: %w", err) } return u.backboneRepo.WithTransaction(func(tx *gorm.DB) error { // Get original backbone for comparison with lock var originalBackbone entity.Backbone if err := tx.Set("gorm:query_option", "FOR UPDATE"). Where("id = ?", id).First(&originalBackbone).Error; err != nil { if err == gorm.ErrRecordNotFound { return fmt.Errorf("backbone not found") } return err } updates := make(map[string]interface{}) devicesToUpdate := make(map[uuid.UUID]bool) devicesToUpdate[originalBackbone.DeviceStartID] = true devicesToUpdate[originalBackbone.DeviceEndID] = true // Validate device type changes if devices are being changed if backbone.DeviceStartID != nil && *backbone.DeviceStartID != originalBackbone.DeviceStartID { var newStartDevice entity.Device if err := tx.Set("gorm:query_option", "FOR UPDATE"). Where("id = ?", *backbone.DeviceStartID).First(&newStartDevice).Error; err != nil { return fmt.Errorf("new start device not found: %w", err) } if newStartDevice.DeviceType != "OTB" { return fmt.Errorf("new start device must be of type OTB, got %s", newStartDevice.DeviceType) } updates["dev_start_id"] = *backbone.DeviceStartID devicesToUpdate[*backbone.DeviceStartID] = true } if backbone.DeviceEndID != nil && *backbone.DeviceEndID != originalBackbone.DeviceEndID { var newEndDevice entity.Device if err := tx.Set("gorm:query_option", "FOR UPDATE"). Where("id = ?", *backbone.DeviceEndID).First(&newEndDevice).Error; err != nil { return fmt.Errorf("new end device not found: %w", err) } if newEndDevice.DeviceType != "OTB" { return fmt.Errorf("new end device must be of type OTB, got %s", newEndDevice.DeviceType) } updates["dev_end_id"] = *backbone.DeviceEndID devicesToUpdate[*backbone.DeviceEndID] = true } // Handle core amount changes if backbone.CoreAmount != nil && *backbone.CoreAmount != originalBackbone.CoreAmount { updates["core_amount"] = *backbone.CoreAmount } if len(updates) == 0 { return fmt.Errorf("no fields to update") } updates["updated_at"] = time.Now() // Update backbone if err := tx.Model(&entity.Backbone{}).Where("id = ?", id).Updates(updates).Error; err != nil { return err } // Update port usage for all affected devices for deviceID := range devicesToUpdate { if err := u.updateDevicePortUsageInTx(tx, deviceID); err != nil { return fmt.Errorf("failed to update device port usage for device %s: %w", deviceID, err) } } return nil }) } func (u *backboneUseCase) updateDevicePortUsageInTx(tx *gorm.DB, deviceID uuid.UUID) error { // Get device with lock var device entity.Device if err := tx.Set("gorm:query_option", "FOR UPDATE"). Where("id = ?", deviceID).First(&device).Error; err != nil { return err } 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 }