Merge pull request 'adding olt' (#28) from feature/responses-v2 into dev

Reviewed-on: winter-access/backend_nam#28
This commit is contained in:
areeqakbr 2025-06-23 03:57:08 +00:00
commit 1ba0df21ed
11 changed files with 683 additions and 0 deletions

View File

@ -0,0 +1,229 @@
package controller
import (
"net/http"
"strconv"
"users_management/m/model/dto/req"
"users_management/m/usecase"
"users_management/m/utils/common"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
)
type OLTController struct {
oltUC usecase.OLTUsecase
rg *gin.RouterGroup
}
func NewOLTController(oltUC usecase.OLTUsecase, rg *gin.RouterGroup) *OLTController {
return &OLTController{
oltUC: oltUC,
rg: rg,
}
}
func (c *OLTController) Route() {
oltGroup := c.rg.Group("/olt")
{
oltGroup.POST("", c.createOLT)
oltGroup.GET("", c.getAllOLTs)
oltGroup.GET("/:id", c.getOLTByID)
oltGroup.PUT("/:id", c.updateOLT)
oltGroup.DELETE("/:id", c.deleteOLT)
oltGroup.POST("/:id/assign-device", c.assignDeviceToOLT)
oltGroup.DELETE("/unassign-device/:deviceId", c.unassignDeviceFromOLT)
oltGroup.GET("/:id/devices", c.getDevicesByOLT)
}
}
func (c *OLTController) createOLT(ctx *gin.Context) {
var oltDTO req.OLTDTO
if err := ctx.ShouldBindJSON(&oltDTO); err != nil {
common.ErrorResponses(ctx, http.StatusBadRequest, err.Error())
return
}
err := c.oltUC.CreateOLT(oltDTO)
if err != nil {
if err.Error() == "OLT name already exists" {
common.ErrorResponses(ctx, http.StatusConflict, err.Error())
return
}
common.ErrorResponses(ctx, http.StatusInternalServerError, err.Error())
return
}
common.SingleResponses(ctx, "OLT created successfully", gin.H{
"olt_name": oltDTO.OLTName,
})
}
func (c *OLTController) getAllOLTs(ctx *gin.Context) {
// Pagination parameters
page, _ := strconv.Atoi(ctx.DefaultQuery("page", "1"))
if page < 1 {
page = 1
}
olts, err := c.oltUC.GetAllOLTs()
if err != nil {
common.ErrorResponses(ctx, http.StatusInternalServerError, err.Error())
return
}
response := gin.H{
"olts": olts,
"total": len(olts),
"page": page,
}
common.SingleResponses(ctx, "OLTs retrieved successfully", response)
}
func (c *OLTController) getOLTByID(ctx *gin.Context) {
idStr := ctx.Param("id")
id, err := uuid.Parse(idStr)
if err != nil {
common.ErrorResponses(ctx, http.StatusBadRequest, "Invalid OLT ID")
return
}
olt, err := c.oltUC.GetOLTByID(id)
if err != nil {
common.ErrorResponses(ctx, http.StatusNotFound, "OLT not found")
return
}
common.SingleResponses(ctx, "OLT details retrieved successfully", olt)
}
func (c *OLTController) updateOLT(ctx *gin.Context) {
idStr := ctx.Param("id")
id, err := uuid.Parse(idStr)
if err != nil {
common.ErrorResponses(ctx, http.StatusBadRequest, "Invalid OLT ID")
return
}
var updateDTO req.UpdateOLTDTO
if err := ctx.ShouldBindJSON(&updateDTO); err != nil {
common.ErrorResponses(ctx, http.StatusBadRequest, err.Error())
return
}
err = c.oltUC.UpdateOLT(id, updateDTO)
if err != nil {
switch err.Error() {
case "OLT not found":
common.ErrorResponses(ctx, http.StatusNotFound, err.Error())
case "OLT name already exists":
common.ErrorResponses(ctx, http.StatusConflict, err.Error())
default:
common.ErrorResponses(ctx, http.StatusInternalServerError, err.Error())
}
return
}
common.SingleResponses(ctx, "OLT updated successfully", gin.H{
"olt_id": id,
})
}
func (c *OLTController) deleteOLT(ctx *gin.Context) {
idStr := ctx.Param("id")
id, err := uuid.Parse(idStr)
if err != nil {
common.ErrorResponses(ctx, http.StatusBadRequest, "Invalid OLT ID")
return
}
err = c.oltUC.DeleteOLT(id)
if err != nil {
if err.Error() == "OLT not found" {
common.ErrorResponses(ctx, http.StatusNotFound, err.Error())
return
}
common.ErrorResponses(ctx, http.StatusInternalServerError, err.Error())
return
}
common.SingleResponses(ctx, "OLT deleted successfully", gin.H{
"olt_id": id,
})
}
func (c *OLTController) assignDeviceToOLT(ctx *gin.Context) {
oltIDStr := ctx.Param("id")
oltID, err := uuid.Parse(oltIDStr)
if err != nil {
common.ErrorResponses(ctx, http.StatusBadRequest, "Invalid OLT ID")
return
}
var assignDTO req.AssignDeviceToOLTDTO
if err := ctx.ShouldBindJSON(&assignDTO); err != nil {
common.ErrorResponses(ctx, http.StatusBadRequest, err.Error())
return
}
err = c.oltUC.AssignDeviceToOLT(oltID, assignDTO.DeviceID)
if err != nil {
if err.Error() == "only ODP devices can be assigned to OLT" {
common.ErrorResponses(ctx, http.StatusBadRequest, err.Error())
return
}
common.ErrorResponses(ctx, http.StatusInternalServerError, err.Error())
return
}
common.SingleResponses(ctx, "Device assigned to OLT successfully", gin.H{
"olt_id": oltID,
"device_id": assignDTO.DeviceID,
})
}
func (c *OLTController) unassignDeviceFromOLT(ctx *gin.Context) {
deviceIDStr := ctx.Param("deviceId")
deviceID, err := uuid.Parse(deviceIDStr)
if err != nil {
common.ErrorResponses(ctx, http.StatusBadRequest, "Invalid device ID")
return
}
err = c.oltUC.UnassignDeviceFromOLT(deviceID)
if err != nil {
common.ErrorResponses(ctx, http.StatusInternalServerError, err.Error())
return
}
common.SingleResponses(ctx, "Device unassigned from OLT successfully", gin.H{
"device_id": deviceID,
})
}
func (c *OLTController) getDevicesByOLT(ctx *gin.Context) {
idStr := ctx.Param("id")
id, err := uuid.Parse(idStr)
if err != nil {
common.ErrorResponses(ctx, http.StatusBadRequest, "Invalid OLT ID")
return
}
devices, err := c.oltUC.GetDevicesByOLT(id)
if err != nil {
if err.Error() == "OLT not found" {
common.ErrorResponses(ctx, http.StatusNotFound, err.Error())
return
}
common.ErrorResponses(ctx, http.StatusInternalServerError, err.Error())
return
}
response := gin.H{
"devices": devices,
"total": len(devices),
}
common.SingleResponses(ctx, "OLT devices retrieved successfully", response)
}

View File

@ -90,6 +90,7 @@ func (s *Server) setupController() {
controller.NewDeviceInspectionController(s.ucManager.NewDeviceInspectionUsecase(), protected).Route()
controller.NewNearestDeviceController(s.ucManager.NewNearestDeviceUsecase(), protected, s.cfg).Route()
controller.NewDeviceDetailsController(s.ucManager.NewDeviceDetailsUsecase(), protected, s.cfg).Route()
controller.NewOLTController(s.ucManager.NewOLTUsecase(), protected).Route()
}

View File

@ -47,6 +47,7 @@ func (im *infraManager) autoMigrate(db *gorm.DB) error {
&entity.DevicePort{},
&entity.CountAssets{},
&entity.ActivityLog{},
&entity.OLT{},
)
}

View File

@ -16,6 +16,7 @@ type RepositoryManager interface {
NewNearestDeviceRepository() repository.NearestDeviceRepo
NewDeviceDetailsRepository() repository.DeviceDetailsRepo
NewOLTRepo() repository.OLTRepo
}
@ -70,4 +71,8 @@ func (rm *repositoryManager) NewActivityLogRepository() repository.ActivityLogRe
func (rm *repositoryManager) NewDeviceInspectionRepository() repository.DeviceInspectionRepo {
return repository.NewDeviceInspectionRepo(rm.infra.Conn())
}
func (rm *repositoryManager) NewOLTRepo() repository.OLTRepo {
return repository.NewOLTRepo(rm.infra.Conn())
}

View File

@ -25,6 +25,7 @@ type UsecaseManager interface {
NewNearestDeviceUsecase() usecase.NearestDeviceUseCase
NewDeviceDetailsUsecase() usecase.DeviceDetailsUseCase
NewOLTUsecase() usecase.OLTUsecase
}
@ -106,4 +107,8 @@ func (um *usecaseManager) NewDeviceInspectionUsecase() usecase.DeviceInspectionU
um.NewActivityLogUsecase(),
service.NewGeocodingService(), // Add the geocoding service
)
}
func (um *usecaseManager) NewOLTUsecase() usecase.OLTUsecase {
return usecase.NewOLTUsecase(um.repo.NewOLTRepo(), um.repo.NewDeviceDetailsRepository(), service.NewGeocodingService())
}

19
model/dto/req/olt.go Normal file
View File

@ -0,0 +1,19 @@
package req
import "github.com/google/uuid"
type OLTDTO struct {
OLTName string `json:"olt_name" validate:"required,min=3,max=100"`
}
type UpdateOLTDTO struct {
OLTName *string `json:"olt_name,omitempty" validate:"omitempty,min=3,max=100"`
}
type AssignDeviceToOLTDTO struct {
DeviceID uuid.UUID `json:"device_id" validate:"required"`
}
type UnassignDeviceFromOLTDTO struct {
DeviceID uuid.UUID `json:"device_id" validate:"required"`
}

39
model/dto/res/olt.go Normal file
View File

@ -0,0 +1,39 @@
package res
import (
"time"
"github.com/google/uuid"
)
type OLTResponse struct {
ID uuid.UUID `json:"id"`
OLTName string `json:"olt_name"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
type OLTDetailResponse struct {
ID uuid.UUID `json:"id"`
OLTName string `json:"olt_name"`
DeviceCount int `json:"device_count"`
Devices []DeviceDetailsResponse `json:"devices"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
type OLTDeviceResponse struct {
ID uuid.UUID `json:"id"`
DeviceCode string `json:"device_code"`
DeviceType string `json:"device_type"`
Address string `json:"address"`
Province *string `json:"province,omitempty"`
City *string `json:"city,omitempty"`
District *string `json:"district,omitempty"`
Status string `json:"status"`
PortAmount int `json:"port_amount"`
PortUsed int `json:"port_used"`
ImageURL *string `json:"image_url,omitempty"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}

View File

@ -32,8 +32,11 @@ type Device struct {
District *string `json:"district,omitempty" gorm:"type:varchar(255)"`
ImageURL *string `json:"image_url,omitempty" gorm:"type:text"`
ImageURLs StringSlice `json:"image_urls" gorm:"type:jsonb"` // Store multiple images as JSONB
OLTID *uuid.UUID `json:"olt_id,omitempty" gorm:"type:uuid"` // Add this field
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
OLT *OLT `json:"olt,omitempty" gorm:"foreignKey:OLTID;references:ID"`
}
func (d *Device) GetAllImageURLs() []string {

20
model/entity/olt.go Normal file
View File

@ -0,0 +1,20 @@
package entity
import (
"time"
"github.com/google/uuid"
)
type OLT struct {
ID uuid.UUID `json:"id" gorm:"type:uuid;primaryKey"`
OLTName string `json:"olt_name" gorm:"unique;not null"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
// Relationships - One OLT can have many devices
Devices []Device `json:"devices,omitempty" gorm:"foreignKey:OLTID;references:ID"`
}
func (OLT) TableName() string {
return "olts"
}

127
repository/olt_repo.go Normal file
View File

@ -0,0 +1,127 @@
package repository
import (
"fmt"
"users_management/m/model/entity"
"users_management/m/model/dto/req"
"github.com/google/uuid"
"gorm.io/gorm"
)
type OLTRepo interface {
Create(olt entity.OLT) error
GetAll() ([]entity.OLT, error)
GetByID(id uuid.UUID) (entity.OLT, error)
GetByIDWithDevices(id uuid.UUID) (entity.OLT, error)
Update(id uuid.UUID, updateDTO req.UpdateOLTDTO) error
Delete(id uuid.UUID) error
AssignDeviceToOLT(oltID, deviceID uuid.UUID) error
UnassignDeviceFromOLT(deviceID uuid.UUID) error
GetByName(name string) (entity.OLT, error)
GetDevicesByOLTID(oltID uuid.UUID) ([]entity.Device, error)
}
type oltRepo struct {
db *gorm.DB
}
func NewOLTRepo(db *gorm.DB) OLTRepo {
return &oltRepo{db: db}
}
func (r *oltRepo) Create(olt entity.OLT) error {
return r.db.Create(&olt).Error
}
func (r *oltRepo) GetAll() ([]entity.OLT, error) {
var olts []entity.OLT
err := r.db.Order("created_at DESC").Find(&olts).Error
return olts, err
}
func (r *oltRepo) GetByID(id uuid.UUID) (entity.OLT, error) {
var olt entity.OLT
err := r.db.Where("id = ?", id).First(&olt).Error
return olt, err
}
func (r *oltRepo) GetByIDWithDevices(id uuid.UUID) (entity.OLT, error) {
var olt entity.OLT
err := r.db.Preload("Devices").
Preload("Devices.DevicePort").
Where("id = ?", id).
First(&olt).Error
return olt, err
}
func (r *oltRepo) Update(id uuid.UUID, updateDTO req.UpdateOLTDTO) error {
updates := make(map[string]interface{})
if updateDTO.OLTName != nil {
updates["olt_name"] = *updateDTO.OLTName
}
if len(updates) == 0 {
return fmt.Errorf("no fields to update")
}
return r.db.Model(&entity.OLT{}).Where("id = ?", id).Updates(updates).Error
}
func (r *oltRepo) Delete(id uuid.UUID) error {
return r.db.Transaction(func(tx *gorm.DB) error {
// First, unassign all devices from this OLT
if err := tx.Model(&entity.Device{}).
Where("olt_id = ?", id).
Update("olt_id", nil).Error; err != nil {
return err
}
// Then delete the OLT
return tx.Where("id = ?", id).Delete(&entity.OLT{}).Error
})
}
func (r *oltRepo) AssignDeviceToOLT(oltID, deviceID uuid.UUID) error {
return r.db.Transaction(func(tx *gorm.DB) error {
// Check if OLT exists
var olt entity.OLT
if err := tx.Where("id = ?", oltID).First(&olt).Error; err != nil {
return fmt.Errorf("OLT not found: %w", err)
}
// Check if device exists and is ODP type
var device entity.Device
if err := tx.Where("id = ?", deviceID).First(&device).Error; err != nil {
return fmt.Errorf("device not found: %w", err)
}
if device.DeviceType != entity.ODP {
return fmt.Errorf("only ODP devices can be assigned to OLT")
}
// Assign device to OLT
return tx.Model(&device).Update("olt_id", oltID).Error
})
}
func (r *oltRepo) UnassignDeviceFromOLT(deviceID uuid.UUID) error {
return r.db.Model(&entity.Device{}).
Where("id = ?", deviceID).
Update("olt_id", nil).Error
}
func (r *oltRepo) GetByName(name string) (entity.OLT, error) {
var olt entity.OLT
err := r.db.Where("olt_name = ?", name).First(&olt).Error
return olt, err
}
func (r *oltRepo) GetDevicesByOLTID(oltID uuid.UUID) ([]entity.Device, error) {
var devices []entity.Device
err := r.db.Preload("DevicePort").
Where("olt_id = ? AND device_type = ?", oltID, entity.ODP).
Find(&devices).Error
return devices, err
}

234
usecase/olt_usecase.go Normal file
View File

@ -0,0 +1,234 @@
package usecase
import (
"errors"
"fmt"
"log"
"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"
"users_management/m/utils/service"
"github.com/go-playground/validator/v10"
"github.com/google/uuid"
)
type OLTUsecase interface {
CreateOLT(oltDTO req.OLTDTO) error
GetAllOLTs() ([]res.OLTResponse, error)
GetOLTByID(id uuid.UUID) (res.OLTDetailResponse, error)
UpdateOLT(id uuid.UUID, updateDTO req.UpdateOLTDTO) error
DeleteOLT(id uuid.UUID) error
AssignDeviceToOLT(oltID, deviceID uuid.UUID) error
UnassignDeviceFromOLT(deviceID uuid.UUID) error
GetDevicesByOLT(oltID uuid.UUID) ([]res.OLTDeviceResponse, error)
}
type oltUsecase struct {
oltRepo repository.OLTRepo
deviceRepo repository.DeviceDetailsRepo
validate *validator.Validate
geocoder service.GeocodingService
}
func NewOLTUsecase(oltRepo repository.OLTRepo, deviceRepo repository.DeviceDetailsRepo, geocoder service.GeocodingService) OLTUsecase {
return &oltUsecase{
oltRepo: oltRepo,
deviceRepo: deviceRepo,
validate: validator.New(),
geocoder: geocoder,
}
}
func (u *oltUsecase) CreateOLT(oltDTO req.OLTDTO) error {
if err := u.validate.Struct(oltDTO); err != nil {
return fmt.Errorf("validation error: %w", err)
}
// Check if OLT name already exists
existingOLT, err := u.oltRepo.GetByName(oltDTO.OLTName)
if err == nil && existingOLT.ID != uuid.Nil {
return errors.New("OLT name already exists")
}
olt := entity.OLT{
ID: uuid.New(),
OLTName: oltDTO.OLTName,
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
}
return u.oltRepo.Create(olt)
}
func (u *oltUsecase) GetAllOLTs() ([]res.OLTResponse, error) {
olts, err := u.oltRepo.GetAll()
if err != nil {
return nil, err
}
var responses []res.OLTResponse
for _, olt := range olts {
response := res.OLTResponse{
ID: olt.ID,
OLTName: olt.OLTName,
CreatedAt: olt.CreatedAt,
UpdatedAt: olt.UpdatedAt,
}
responses = append(responses, response)
}
return responses, nil
}
func (u *oltUsecase) GetOLTByID(id uuid.UUID) (res.OLTDetailResponse, error) {
// First, check if OLT exists using the basic GetByID method
olt, err := u.oltRepo.GetByID(id)
if err != nil {
log.Printf("OLT not found: %v", err)
return res.OLTDetailResponse{}, errors.New("OLT not found")
}
log.Printf("OLT found: %+v", olt)
// Try to get OLT with devices, but fallback to basic info if it fails
oltWithDevices, err := u.oltRepo.GetByIDWithDevices(id)
if err != nil {
log.Printf("Error getting OLT with devices, using basic OLT info: %v", err)
// Use the basic OLT info we already retrieved
oltWithDevices = olt
oltWithDevices.Devices = []entity.Device{} // Ensure devices is empty slice, not nil
}
log.Printf("OLT retrieved with devices: %+v", oltWithDevices)
// Convert devices to device details responses
var deviceResponses []res.DeviceDetailsResponse
for _, device := range oltWithDevices.Devices {
log.Printf("Processing device: %s", device.DeviceCode)
// Get the device details using the existing method which properly loads DevicePort
deviceDetails, err := u.deviceRepo.GetByID(device.ID)
if err != nil {
log.Printf("Error getting device details for %s: %v", device.DeviceCode, err)
continue
}
deviceResponse, err := helper.ConvertToDeviceDetailsResponse(deviceDetails, u.geocoder)
if err != nil {
log.Printf("Error converting device response for %s: %v", device.DeviceCode, err)
continue
}
deviceResponses = append(deviceResponses, deviceResponse)
}
// Initialize empty slice if nil to ensure JSON response shows empty array instead of null
if deviceResponses == nil {
deviceResponses = []res.DeviceDetailsResponse{}
}
response := res.OLTDetailResponse{
ID: oltWithDevices.ID,
OLTName: oltWithDevices.OLTName,
DeviceCount: len(deviceResponses),
Devices: deviceResponses,
CreatedAt: oltWithDevices.CreatedAt,
UpdatedAt: oltWithDevices.UpdatedAt,
}
return response, nil
}
func (u *oltUsecase) UpdateOLT(id uuid.UUID, updateDTO req.UpdateOLTDTO) error {
if err := u.validate.Struct(updateDTO); err != nil {
return fmt.Errorf("validation error: %w", err)
}
// Check if OLT exists
_, err := u.oltRepo.GetByID(id)
if err != nil {
return errors.New("OLT not found")
}
// Check if new name already exists (if name is being updated)
if updateDTO.OLTName != nil {
existingOLT, err := u.oltRepo.GetByName(*updateDTO.OLTName)
if err == nil && existingOLT.ID != id {
return errors.New("OLT name already exists")
}
}
return u.oltRepo.Update(id, updateDTO)
}
func (u *oltUsecase) DeleteOLT(id uuid.UUID) error {
// Check if OLT exists
_, err := u.oltRepo.GetByID(id)
if err != nil {
return errors.New("OLT not found")
}
return u.oltRepo.Delete(id)
}
func (u *oltUsecase) AssignDeviceToOLT(oltID, deviceID uuid.UUID) error {
return u.oltRepo.AssignDeviceToOLT(oltID, deviceID)
}
func (u *oltUsecase) UnassignDeviceFromOLT(deviceID uuid.UUID) error {
return u.oltRepo.UnassignDeviceFromOLT(deviceID)
}
func (u *oltUsecase) GetDevicesByOLT(oltID uuid.UUID) ([]res.OLTDeviceResponse, error) {
// Check if OLT exists
_, err := u.oltRepo.GetByID(oltID)
if err != nil {
return nil, errors.New("OLT not found")
}
devices, err := u.oltRepo.GetDevicesByOLTID(oltID)
if err != nil {
return nil, err
}
var responses []res.OLTDeviceResponse
for _, device := range devices {
// Get address using geocoder
address := ""
if u.geocoder != nil {
addr, err := u.geocoder.GetAddressFromCoordinates(device.Latitude, device.Longitude)
if err == nil {
address = addr
}
}
// Get port usage from device port repository
portUsed := 0
portUsageInfo, _, err := u.deviceRepo.GetPortUsageByDevice(device.ID)
if err == nil {
portUsed = portUsageInfo
}
response := res.OLTDeviceResponse{
ID: device.ID,
DeviceCode: device.DeviceCode,
DeviceType: string(device.DeviceType),
Address: address,
Province: device.Province,
City: device.City,
District: device.District,
Status: string(device.Status),
PortAmount: device.PortAmount,
PortUsed: portUsed,
ImageURL: device.ImageURL,
CreatedAt: device.CreatedAt,
UpdatedAt: device.UpdatedAt,
}
responses = append(responses, response)
}
return responses, nil
}