fix: make fishbone dev_end_id optional (nullable)

DeviceEndID was uuid.UUID (non-nullable) in entity, DTO, and response.
Sending empty string from the frontend caused "invalid uuid" parse error.

Changed to *uuid.UUID throughout. CreateFishbone now skips end device
lookup, type validation, port check, and port-usage update when
DeviceEndID is nil. UpdateFishbone pointer comparisons fixed accordingly.

Run once on DB: ALTER TABLE fishbone ALTER COLUMN dev_end_id DROP NOT NULL;

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
unknown 2026-04-11 16:55:46 +07:00
parent 128bc30680
commit 9ae9de471d
5 changed files with 36 additions and 36 deletions

View File

@ -6,7 +6,7 @@ type FishboneDTO struct {
FishboneCode string `json:"fishbone_code" validate:"required,min=3"` FishboneCode string `json:"fishbone_code" validate:"required,min=3"`
BackboneID uuid.UUID `json:"bb_id" validate:"required"` BackboneID uuid.UUID `json:"bb_id" validate:"required"`
DeviceStartID uuid.UUID `json:"dev_start_id" validate:"required"` DeviceStartID uuid.UUID `json:"dev_start_id" validate:"required"`
DeviceEndID uuid.UUID `json:"dev_end_id" validate:"required"` DeviceEndID *uuid.UUID `json:"dev_end_id" validate:"omitempty"`
CoreAmount int `json:"core_amount" validate:"required,min=1"` CoreAmount int `json:"core_amount" validate:"required,min=1"`
} }

View File

@ -13,7 +13,7 @@ type FishboneResponse struct {
DeviceStart string `json:"device_start"` DeviceStart string `json:"device_start"`
DeviceEnd string `json:"device_end"` DeviceEnd string `json:"device_end"`
DeviceStartID uuid.UUID `json:"device_start_id"` DeviceStartID uuid.UUID `json:"device_start_id"`
DeviceEndID uuid.UUID `json:"device_end_id"` DeviceEndID *uuid.UUID `json:"device_end_id"`
CoreAmount int `json:"core_amount"` CoreAmount int `json:"core_amount"`
CreatedAt time.Time `json:"created_at"` CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"` UpdatedAt time.Time `json:"updated_at"`
@ -23,7 +23,7 @@ type FishboneDetailResponse struct {
ID uuid.UUID `json:"id"` ID uuid.UUID `json:"id"`
FishboneCode string `json:"fishbone_code"` FishboneCode string `json:"fishbone_code"`
DeviceStartID uuid.UUID `json:"device_start_id"` DeviceStartID uuid.UUID `json:"device_start_id"`
DeviceEndID uuid.UUID `json:"device_end_id"` DeviceEndID *uuid.UUID `json:"device_end_id"`
BackboneCode string `json:"backbone_code"` BackboneCode string `json:"backbone_code"`
CoreAmount int `json:"core_amount"` CoreAmount int `json:"core_amount"`
CreatedAt time.Time `json:"created_at"` CreatedAt time.Time `json:"created_at"`

View File

@ -10,7 +10,7 @@ type Fishbone struct {
FishboneCode string `json:"fishbone_code" gorm:"unique"` FishboneCode string `json:"fishbone_code" gorm:"unique"`
BackboneID uuid.UUID `json:"bb_id" gorm:"type:uuid;column:bb_id"` BackboneID uuid.UUID `json:"bb_id" gorm:"type:uuid;column:bb_id"`
DeviceStartID uuid.UUID `json:"dev_start_id" gorm:"type:uuid;column:dev_start_id"` DeviceStartID uuid.UUID `json:"dev_start_id" gorm:"type:uuid;column:dev_start_id"`
DeviceEndID uuid.UUID `json:"dev_end_id" gorm:"type:uuid;column:dev_end_id"` DeviceEndID *uuid.UUID `json:"dev_end_id" gorm:"type:uuid;column:dev_end_id"`
CoreAmount int `json:"core_amount" gorm:"column:core_amount"` CoreAmount int `json:"core_amount" gorm:"column:core_amount"`
CreatedAt time.Time `json:"created_at"` CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"` UpdatedAt time.Time `json:"updated_at"`

View File

@ -72,45 +72,23 @@ func (u *fishboneUseCase) CreateFishbone(fishbone req.FishboneDTO) error {
return fmt.Errorf("start device not found: %w", err) 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 // Validate device types
if strings.ToLower(string(startDevice.DeviceType)) != "closure" { if strings.ToLower(string(startDevice.DeviceType)) != "closure" {
return fmt.Errorf("start device must be of type closure, got %s", startDevice.DeviceType) return fmt.Errorf("start device must be of type closure, got %s", startDevice.DeviceType)
} }
if strings.ToUpper(string(endDevice.DeviceType)) != "ODP" { // Check port availability for start device
return fmt.Errorf("end device must be of type ODP, got %s", endDevice.DeviceType)
}
// Check port availability with locking
var startDevicePort entity.DevicePort var startDevicePort entity.DevicePort
if err := tx.Set("gorm:query_option", "FOR UPDATE"). if err := tx.Set("gorm:query_option", "FOR UPDATE").
Where("device_id = ?", fishbone.DeviceStartID).First(&startDevicePort).Error; err != nil { Where("device_id = ?", fishbone.DeviceStartID).First(&startDevicePort).Error; err != nil {
return fmt.Errorf("start device port record not found: %w", err) 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" { if startDevicePort.PortAvailable < 1 && startDevice.DeviceType == "ODP" {
return fmt.Errorf("start device has no available ports (available: %d, required: 1)", return fmt.Errorf("start device has no available ports (available: %d, required: 1)",
startDevicePort.PortAvailable) 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{ newFishbone := entity.Fishbone{
ID: uuid.New(), ID: uuid.New(),
FishboneCode: fishbone.FishboneCode, FishboneCode: fishbone.FishboneCode,
@ -127,13 +105,33 @@ func (u *fishboneUseCase) CreateFishbone(fishbone req.FishboneDTO) error {
return err return err
} }
// Update port usage for both devices // Update port usage for start device
if err := u.updateDevicePortUsageInTx(tx, fishbone.DeviceStartID); err != nil { if err := u.updateDevicePortUsageInTx(tx, fishbone.DeviceStartID); err != nil {
return fmt.Errorf("failed to update start device port usage: %w", err) return fmt.Errorf("failed to update start device port usage: %w", err)
} }
if err := u.updateDevicePortUsageInTx(tx, fishbone.DeviceEndID); err != nil { // Only validate and update end device if provided (it is optional)
return fmt.Errorf("failed to update end device port usage: %w", err) if fishbone.DeviceEndID != nil {
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)
}
if strings.ToUpper(string(endDevice.DeviceType)) != "ODP" {
return fmt.Errorf("end device must be of type ODP, got %s", endDevice.DeviceType)
}
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)
}
if endDevicePort.PortAvailable < fishbone.CoreAmount {
return fmt.Errorf("end device has insufficient available ports (available: %d, required: %d)",
endDevicePort.PortAvailable, fishbone.CoreAmount)
}
if err := u.updateDevicePortUsageInTx(tx, *fishbone.DeviceEndID); err != nil {
return fmt.Errorf("failed to update end device port usage: %w", err)
}
} }
return nil return nil
@ -228,7 +226,9 @@ func (u *fishboneUseCase) UpdateFishbone(id uuid.UUID, fishbone req.UpdateFishbo
updates := make(map[string]interface{}) updates := make(map[string]interface{})
devicesToUpdate := make(map[uuid.UUID]bool) devicesToUpdate := make(map[uuid.UUID]bool)
devicesToUpdate[originalFishbone.DeviceStartID] = true devicesToUpdate[originalFishbone.DeviceStartID] = true
devicesToUpdate[originalFishbone.DeviceEndID] = true if originalFishbone.DeviceEndID != nil {
devicesToUpdate[*originalFishbone.DeviceEndID] = true
}
// Validate device type changes if devices are being changed // Validate device type changes if devices are being changed
if fishbone.DeviceStartID != nil && *fishbone.DeviceStartID != originalFishbone.DeviceStartID { if fishbone.DeviceStartID != nil && *fishbone.DeviceStartID != originalFishbone.DeviceStartID {
@ -244,7 +244,7 @@ func (u *fishboneUseCase) UpdateFishbone(id uuid.UUID, fishbone req.UpdateFishbo
devicesToUpdate[*fishbone.DeviceStartID] = true devicesToUpdate[*fishbone.DeviceStartID] = true
} }
if fishbone.DeviceEndID != nil && *fishbone.DeviceEndID != originalFishbone.DeviceEndID { if fishbone.DeviceEndID != nil && (originalFishbone.DeviceEndID == nil || *fishbone.DeviceEndID != *originalFishbone.DeviceEndID) {
var newEndDevice entity.Device var newEndDevice entity.Device
if err := tx.Set("gorm:query_option", "FOR UPDATE"). if err := tx.Set("gorm:query_option", "FOR UPDATE").
Where("id = ?", *fishbone.DeviceEndID).First(&newEndDevice).Error; err != nil { Where("id = ?", *fishbone.DeviceEndID).First(&newEndDevice).Error; err != nil {

View File

@ -19,7 +19,7 @@ func ConvertToFishboneResponses(fishbones []entity.Fishbone, totalFishbone map[u
DeviceStart: fishbone.DeviceStart.DeviceCode, DeviceStart: fishbone.DeviceStart.DeviceCode,
DeviceEnd: fishbone.DeviceEnd.DeviceCode, DeviceEnd: fishbone.DeviceEnd.DeviceCode,
DeviceStartID: fishbone.DeviceStart.ID, DeviceStartID: fishbone.DeviceStart.ID,
DeviceEndID: fishbone.DeviceEnd.ID, DeviceEndID: fishbone.DeviceEndID,
CoreAmount: fishbone.CoreAmount, CoreAmount: fishbone.CoreAmount,
CreatedAt: fishbone.CreatedAt, CreatedAt: fishbone.CreatedAt,
UpdatedAt: fishbone.UpdatedAt, UpdatedAt: fishbone.UpdatedAt,
@ -36,7 +36,7 @@ func ConvertToFishboneDetailResponse(fishbone entity.Fishbone) res.FishboneDetai
ID: fishbone.ID, ID: fishbone.ID,
FishboneCode: fishbone.FishboneCode, FishboneCode: fishbone.FishboneCode,
DeviceStartID: fishbone.DeviceStart.ID, DeviceStartID: fishbone.DeviceStart.ID,
DeviceEndID: fishbone.DeviceEnd.ID, DeviceEndID: fishbone.DeviceEndID,
BackboneCode: fishbone.Backbone.BackboneCode, BackboneCode: fishbone.Backbone.BackboneCode,
CoreAmount: fishbone.CoreAmount, CoreAmount: fishbone.CoreAmount,
CreatedAt: fishbone.CreatedAt, CreatedAt: fishbone.CreatedAt,
@ -56,7 +56,7 @@ func ConvertToSimpleFishboneResponses(fishbones []entity.Fishbone) []res.Fishbon
DeviceStart: fishbone.DeviceStart.DeviceCode, DeviceStart: fishbone.DeviceStart.DeviceCode,
DeviceEnd: fishbone.DeviceEnd.DeviceCode, DeviceEnd: fishbone.DeviceEnd.DeviceCode,
DeviceStartID: fishbone.DeviceStart.ID, DeviceStartID: fishbone.DeviceStart.ID,
DeviceEndID: fishbone.DeviceEnd.ID, DeviceEndID: fishbone.DeviceEndID,
CoreAmount: fishbone.CoreAmount, CoreAmount: fishbone.CoreAmount,
CreatedAt: fishbone.CreatedAt, CreatedAt: fishbone.CreatedAt,
UpdatedAt: fishbone.UpdatedAt, UpdatedAt: fishbone.UpdatedAt,