package usecase import ( "errors" "fmt" "strings" "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 FishboneUseCase interface { CreateFishbone(fishbone req.FishboneDTO) error GetAllFishbone() ([]res.FishboneResponse, error) GetByID(id uuid.UUID) (res.FishboneDetailResponse, error) UpdateFishbone(id uuid.UUID, fishbone req.UpdateFishboneDTO) error DeleteFishbone(id uuid.UUID) error GetFishboneStats() (map[string]interface{}, error) GetByBackboneID(backboneID uuid.UUID) ([]res.FishboneResponse, error) } type fishboneUseCase struct { fishboneRepo repository.FishboneRepo backboneRepo repository.BackboneRepo deviceDetailsRepo repository.DeviceDetailsRepo // Add this field validate *validator.Validate } func NewFishboneUseCase(fishboneRepo repository.FishboneRepo, backboneRepo repository.BackboneRepo, deviceDetailsRepo repository.DeviceDetailsRepo) FishboneUseCase { return &fishboneUseCase{ fishboneRepo: fishboneRepo, backboneRepo: backboneRepo, deviceDetailsRepo: deviceDetailsRepo, // Initialize the field validate: validator.New(), } } func (u *fishboneUseCase) GetByBackboneID(backboneID uuid.UUID) ([]res.FishboneResponse, error) { fishbones, err := u.fishboneRepo.GetByBackboneID(backboneID) if err != nil { return nil, err } fishboneResp, err := helper.ConvertToFishboneResponses(fishbones, nil) if err != nil { return nil, err } return fishboneResp, nil } func (u *fishboneUseCase) CreateFishbone(fishbone req.FishboneDTO) error { err := u.validate.Struct(fishbone) if err != nil { return fmt.Errorf("validation error: %w", err) } return u.fishboneRepo.WithTransaction(func(tx *gorm.DB) error { // Lock device records to prevent race conditions var startDevice entity.Device if err := tx.Set("gorm:query_option", "FOR UPDATE"). Where("id = ?", fishbone.DeviceStartID).First(&startDevice).Error; err != nil { return fmt.Errorf("start device not found: %w", err) } var endDevice entity.Device if err := tx.Set("gorm:query_option", "FOR UPDATE"). Where("id = ?", fishbone.DeviceEndID).First(&endDevice).Error; err != nil { return fmt.Errorf("end device not found: %w", err) } // Validate device types if strings.ToLower(string(startDevice.DeviceType)) != "closure" { return fmt.Errorf("start device must be of type closure, got %s", startDevice.DeviceType) } if strings.ToUpper(string(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 && startDevice.DeviceType == "ODP" { return fmt.Errorf("start device has no available ports (available: %d, required: 1)", startDevicePort.PortAvailable) } if endDevicePort.PortAvailable < fishbone.CoreAmount { return fmt.Errorf("end device has insufficient available ports (available: %d, required: %d)", endDevicePort.PortAvailable, fishbone.CoreAmount) } newFishbone := entity.Fishbone{ ID: uuid.New(), FishboneCode: fishbone.FishboneCode, BackboneID: fishbone.BackboneID, DeviceStartID: fishbone.DeviceStartID, DeviceEndID: fishbone.DeviceEndID, CoreAmount: fishbone.CoreAmount, CreatedAt: time.Now(), UpdatedAt: time.Now(), } // Create fishbone if err := tx.Create(&newFishbone).Error; err != nil { return err } // Update port usage for both devices if err := u.updateDevicePortUsageInTx(tx, fishbone.DeviceStartID); err != nil { return fmt.Errorf("failed to update start device port usage: %w", err) } if err := u.updateDevicePortUsageInTx(tx, fishbone.DeviceEndID); err != nil { return fmt.Errorf("failed to update end device port usage: %w", err) } return nil }) } func (u *fishboneUseCase) updateDevicePortUsageInTx(tx *gorm.DB, deviceID uuid.UUID) error { // Get device with lock var device entity.Device if err := tx.Set("gorm:query_option", "FOR UPDATE"). Where("id = ?", deviceID).First(&device).Error; err != nil { return err } var portUsed int var customerCount int switch device.DeviceType { case "closure": var fishboneCount int64 if err := tx.Model(&entity.Fishbone{}). Where("dev_start_id = ?", deviceID). Count(&fishboneCount).Error; err != nil { return err } portUsed = int(fishboneCount) customerCount = 0 case "ODP": var totalCores int64 if err := tx.Model(&entity.Fishbone{}). Where("dev_end_id = ?", deviceID). Select("COALESCE(SUM(core_amount), 0)"). Scan(&totalCores).Error; err != nil { return err } portUsed = int(totalCores) customerCount = portUsed // For ODP, customer count equals port_used } portAvailable := device.PortAmount - portUsed if portAvailable < 0 { portAvailable = 0 } 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) { fishbones, err := u.fishboneRepo.GetAll() if err != nil { return nil, err } return helper.ConvertToSimpleFishboneResponses(fishbones), nil } func (u *fishboneUseCase) GetByID(id uuid.UUID) (res.FishboneDetailResponse, error) { fishbone, err := u.fishboneRepo.GetByIDWithRelations(id) if err != nil { return res.FishboneDetailResponse{}, err } // Convert to detailed response using helper return helper.ConvertToFishboneDetailResponse(fishbone), nil } func (u *fishboneUseCase) UpdateFishbone(id uuid.UUID, fishbone req.UpdateFishboneDTO) error { err := u.validate.Struct(fishbone) if err != nil { return fmt.Errorf("validation error: %w", err) } return u.fishboneRepo.WithTransaction(func(tx *gorm.DB) error { // Get original fishbone for comparison with lock var originalFishbone entity.Fishbone if err := tx.Set("gorm:query_option", "FOR UPDATE"). Where("id = ?", id).First(&originalFishbone).Error; err != nil { if err == gorm.ErrRecordNotFound { return fmt.Errorf("fishbone not found") } return err } updates := make(map[string]interface{}) devicesToUpdate := make(map[uuid.UUID]bool) devicesToUpdate[originalFishbone.DeviceStartID] = true devicesToUpdate[originalFishbone.DeviceEndID] = true // Validate device type changes if devices are being changed if fishbone.DeviceStartID != nil && *fishbone.DeviceStartID != originalFishbone.DeviceStartID { var newStartDevice entity.Device if err := tx.Set("gorm:query_option", "FOR UPDATE"). Where("id = ?", *fishbone.DeviceStartID).First(&newStartDevice).Error; err != nil { return fmt.Errorf("new start device not found: %w", err) } if newStartDevice.DeviceType != "closure" { return fmt.Errorf("new start device must be of type closure, got %s", newStartDevice.DeviceType) } updates["dev_start_id"] = *fishbone.DeviceStartID devicesToUpdate[*fishbone.DeviceStartID] = true } if fishbone.DeviceEndID != nil && *fishbone.DeviceEndID != originalFishbone.DeviceEndID { var newEndDevice entity.Device if err := tx.Set("gorm:query_option", "FOR UPDATE"). Where("id = ?", *fishbone.DeviceEndID).First(&newEndDevice).Error; err != nil { return fmt.Errorf("new end device not found: %w", err) } if newEndDevice.DeviceType != "ODP" { return fmt.Errorf("new end device must be of type ODP, got %s", newEndDevice.DeviceType) } updates["dev_end_id"] = *fishbone.DeviceEndID devicesToUpdate[*fishbone.DeviceEndID] = true } // Handle core amount changes if fishbone.CoreAmount != nil && *fishbone.CoreAmount != originalFishbone.CoreAmount { updates["core_amount"] = *fishbone.CoreAmount } if fishbone.BackboneID != nil { // Validate backbone exists var backbone entity.Backbone if err := tx.Where("id = ?", *fishbone.BackboneID).First(&backbone).Error; err != nil { return fmt.Errorf("backbone not found: %w", err) } updates["bb_id"] = *fishbone.BackboneID } if fishbone.FishboneCode != nil { updates["fishbone_code"] = *fishbone.FishboneCode } if len(updates) == 0 { return fmt.Errorf("no fields to update") } updates["updated_at"] = time.Now() // Update fishbone if err := tx.Model(&entity.Fishbone{}).Where("id = ?", id).Updates(updates).Error; err != nil { return err } // Update port usage for all affected devices for deviceID := range devicesToUpdate { if err := u.updateDevicePortUsageInTx(tx, deviceID); err != nil { return fmt.Errorf("failed to update device port usage for device %s: %w", deviceID, err) } } return nil }) } func (u *fishboneUseCase) DeleteFishbone(id uuid.UUID) error { // Check if fishbone exists exists, err := u.fishboneRepo.CheckFishboneExists(id) if err != nil { return err } if !exists { return errors.New("fishbone not found") } return u.fishboneRepo.Delete(id) } func (u *fishboneUseCase) GetFishboneStats() (map[string]interface{}, error) { fishbones, err := u.fishboneRepo.GetAll() if err != nil { return nil, err } return helper.GetFishboneStats(fishbones), nil }