From 9ae9de471d29d1cf33cede0a03c5e62b043b4911 Mon Sep 17 00:00:00 2001 From: unknown Date: Sat, 11 Apr 2026 16:55:46 +0700 Subject: [PATCH] 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 --- model/dto/req/fishbone_dto.go | 2 +- model/dto/res/fishbone_res.go | 4 +-- model/entity/fishbone.go | 2 +- usecase/fishbone_usecase.go | 58 +++++++++++++++---------------- utils/helper/fishboneHelperRes.go | 6 ++-- 5 files changed, 36 insertions(+), 36 deletions(-) diff --git a/model/dto/req/fishbone_dto.go b/model/dto/req/fishbone_dto.go index 1a113dc..7001736 100644 --- a/model/dto/req/fishbone_dto.go +++ b/model/dto/req/fishbone_dto.go @@ -6,7 +6,7 @@ type FishboneDTO struct { FishboneCode string `json:"fishbone_code" validate:"required,min=3"` BackboneID uuid.UUID `json:"bb_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"` } diff --git a/model/dto/res/fishbone_res.go b/model/dto/res/fishbone_res.go index bd29388..9996037 100644 --- a/model/dto/res/fishbone_res.go +++ b/model/dto/res/fishbone_res.go @@ -13,7 +13,7 @@ type FishboneResponse struct { DeviceStart string `json:"device_start"` DeviceEnd string `json:"device_end"` 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"` CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at"` @@ -23,7 +23,7 @@ type FishboneDetailResponse struct { ID uuid.UUID `json:"id"` FishboneCode string `json:"fishbone_code"` 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"` CoreAmount int `json:"core_amount"` CreatedAt time.Time `json:"created_at"` diff --git a/model/entity/fishbone.go b/model/entity/fishbone.go index 788d7aa..c30e78f 100644 --- a/model/entity/fishbone.go +++ b/model/entity/fishbone.go @@ -10,7 +10,7 @@ type Fishbone struct { FishboneCode string `json:"fishbone_code" gorm:"unique"` 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"` - 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"` CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at"` diff --git a/usecase/fishbone_usecase.go b/usecase/fishbone_usecase.go index fa01e0e..a4c9a12 100644 --- a/usecase/fishbone_usecase.go +++ b/usecase/fishbone_usecase.go @@ -72,45 +72,23 @@ func (u *fishboneUseCase) CreateFishbone(fishbone req.FishboneDTO) error { 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 + // Check port availability for start device 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)", + 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, @@ -127,13 +105,33 @@ func (u *fishboneUseCase) CreateFishbone(fishbone req.FishboneDTO) error { return err } - // Update port usage for both devices + // Update port usage for start device 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) + // Only validate and update end device if provided (it is optional) + 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 @@ -228,7 +226,9 @@ func (u *fishboneUseCase) UpdateFishbone(id uuid.UUID, fishbone req.UpdateFishbo updates := make(map[string]interface{}) devicesToUpdate := make(map[uuid.UUID]bool) 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 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 } - if fishbone.DeviceEndID != nil && *fishbone.DeviceEndID != originalFishbone.DeviceEndID { + if fishbone.DeviceEndID != nil && (originalFishbone.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 { diff --git a/utils/helper/fishboneHelperRes.go b/utils/helper/fishboneHelperRes.go index 21f1dab..323091b 100644 --- a/utils/helper/fishboneHelperRes.go +++ b/utils/helper/fishboneHelperRes.go @@ -19,7 +19,7 @@ func ConvertToFishboneResponses(fishbones []entity.Fishbone, totalFishbone map[u DeviceStart: fishbone.DeviceStart.DeviceCode, DeviceEnd: fishbone.DeviceEnd.DeviceCode, DeviceStartID: fishbone.DeviceStart.ID, - DeviceEndID: fishbone.DeviceEnd.ID, + DeviceEndID: fishbone.DeviceEndID, CoreAmount: fishbone.CoreAmount, CreatedAt: fishbone.CreatedAt, UpdatedAt: fishbone.UpdatedAt, @@ -36,7 +36,7 @@ func ConvertToFishboneDetailResponse(fishbone entity.Fishbone) res.FishboneDetai ID: fishbone.ID, FishboneCode: fishbone.FishboneCode, DeviceStartID: fishbone.DeviceStart.ID, - DeviceEndID: fishbone.DeviceEnd.ID, + DeviceEndID: fishbone.DeviceEndID, BackboneCode: fishbone.Backbone.BackboneCode, CoreAmount: fishbone.CoreAmount, CreatedAt: fishbone.CreatedAt, @@ -56,7 +56,7 @@ func ConvertToSimpleFishboneResponses(fishbones []entity.Fishbone) []res.Fishbon DeviceStart: fishbone.DeviceStart.DeviceCode, DeviceEnd: fishbone.DeviceEnd.DeviceCode, DeviceStartID: fishbone.DeviceStart.ID, - DeviceEndID: fishbone.DeviceEnd.ID, + DeviceEndID: fishbone.DeviceEndID, CoreAmount: fishbone.CoreAmount, CreatedAt: fishbone.CreatedAt, UpdatedAt: fishbone.UpdatedAt,