adding radiation odp search
This commit is contained in:
parent
3d5ce0dc72
commit
1dbd96eebd
|
|
@ -0,0 +1,66 @@
|
||||||
|
# PROJECT KNOWLEDGE BASE
|
||||||
|
|
||||||
|
**Generated:** 2026-04-11
|
||||||
|
**Commit:** 3d5ce0d
|
||||||
|
**Branch:** dev
|
||||||
|
|
||||||
|
## OVERVIEW
|
||||||
|
Go backend API for telecom network asset management (NAM). Manages ODP/OTB/OLT/Tower infrastructure with Gin + GORM.
|
||||||
|
|
||||||
|
## STRUCTURE
|
||||||
|
```
|
||||||
|
backend_nam/
|
||||||
|
├── config/ # Environment config (DB, JWT, API)
|
||||||
|
├── delivery/ # HTTP layer (server.go, controllers/)
|
||||||
|
├── manager/ # Dependency injection wiring
|
||||||
|
├── middleware/ # Auth, CORS, RBAC, rate limiting, logging
|
||||||
|
├── model/ # DTOs (req/res) + Entities
|
||||||
|
├── repository/ # Database operations
|
||||||
|
├── usecase/ # Business logic
|
||||||
|
├── utils/ # Helpers, services, migrations, common
|
||||||
|
├── main.go # Entry point
|
||||||
|
└── go.mod
|
||||||
|
```
|
||||||
|
|
||||||
|
## WHERE TO LOOK
|
||||||
|
| Task | Location | Notes |
|
||||||
|
|------|----------|-------|
|
||||||
|
| Add API endpoint | delivery/controller/ | New {name}_controller.go |
|
||||||
|
| Business logic | usecase/ | New {name}_usecase.go |
|
||||||
|
| DB operations | repository/ | New {name}_repo.go |
|
||||||
|
| Data models | model/entity/ | GORM structs |
|
||||||
|
| Config changes | config/config.go | env vars |
|
||||||
|
|
||||||
|
## CONVENTIONS
|
||||||
|
- **Package path**: `users_management/m/...`
|
||||||
|
- **File naming**: snake_case (`device_usecase.go`)
|
||||||
|
- **Interface naming**: `{Entity}Repo`, `{Entity}UseCase`
|
||||||
|
- **Constructor**: `New{Entity}{Layer}(deps) *{entity}{Layer}`
|
||||||
|
- **GORM tags**: `json:"field" gorm:"type:uuid;primaryKey"`
|
||||||
|
- **Response format**: `common.SingleResponses(c, "message", data)`
|
||||||
|
- **Error format**: `common.ErrorResponses(c, http.StatusBadRequest, "message")`
|
||||||
|
|
||||||
|
## ANTI-PATTERNS (THIS PROJECT)
|
||||||
|
- **DO NOT** use `as any` - strict typing required
|
||||||
|
- **DO NOT** skip validation in usecases - use `validator.New()`
|
||||||
|
- **DO NOT** commit `.env` files
|
||||||
|
- **DO NOT** hardcode file paths - use constants in helper/
|
||||||
|
|
||||||
|
## UNIQUE STYLES
|
||||||
|
- **Auth middleware**: Conditional based on `USER_AUTH_ENABLED` env var
|
||||||
|
- **Image handling**: Local filesystem at `./uploads/` + `/uploads/` static route
|
||||||
|
- **Bulk operations**: Return `{Successful, Failed, TotalRequested, Errors[], Results[]}`
|
||||||
|
- **DevicePort auto-create**: Created automatically when Device is created
|
||||||
|
|
||||||
|
## COMMANDS
|
||||||
|
```bash
|
||||||
|
go run main.go # Local dev
|
||||||
|
go build -o main . # Build binary
|
||||||
|
docker build -t nam-backend . # Docker build
|
||||||
|
```
|
||||||
|
|
||||||
|
## NOTES
|
||||||
|
- Port: `:5678` (Dockerfile EXPOSE)
|
||||||
|
- UUID primary keys everywhere
|
||||||
|
- Geocoding service with caching layer
|
||||||
|
- Activity logging middleware captures all mutations
|
||||||
|
|
@ -0,0 +1,30 @@
|
||||||
|
## OVERVIEW
|
||||||
|
High-complexity controller layer: 17 controllers wired to usecases, router groups, and config.
|
||||||
|
|
||||||
|
## WHERE TO LOOK
|
||||||
|
The 17 controller files:
|
||||||
|
- activity_logs_controller.go
|
||||||
|
- auth_controller.go
|
||||||
|
- backbone_controller.go
|
||||||
|
- cable_connections_controller.go
|
||||||
|
- count_assets_controller.go
|
||||||
|
- creatin_admin.go
|
||||||
|
- device_details.go
|
||||||
|
- device_inspections_controller.go
|
||||||
|
- devicePort_controller.go
|
||||||
|
- devices_controller.go
|
||||||
|
- fishbone_controller.go
|
||||||
|
- nearest_device_controller.go
|
||||||
|
- olt_controller.go
|
||||||
|
- tower_controller.go
|
||||||
|
- user_management_controller.go
|
||||||
|
- user_registration.go
|
||||||
|
- users_controller.go
|
||||||
|
|
||||||
|
Pattern: struct {Entity}Controller, New{Entity}Controller(usecase, routerGroup, config), Route() method.
|
||||||
|
|
||||||
|
## ANTI-PATTERNS
|
||||||
|
- Put business logic in controllers; delegate to usecases
|
||||||
|
- Access DB directly in controllers
|
||||||
|
- Skip input validation; use validator
|
||||||
|
- Duplicate route wiring
|
||||||
|
|
@ -14,161 +14,196 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
type NearestDeviceController struct {
|
type NearestDeviceController struct {
|
||||||
nearestDeviceUC usecase.NearestDeviceUseCase
|
nearestDeviceUC usecase.NearestDeviceUseCase
|
||||||
rg *gin.RouterGroup
|
rg *gin.RouterGroup
|
||||||
cfg *config.Config
|
cfg *config.Config
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewNearestDeviceController(nearestDeviceUC usecase.NearestDeviceUseCase, rg *gin.RouterGroup, cfg *config.Config) *NearestDeviceController {
|
func NewNearestDeviceController(nearestDeviceUC usecase.NearestDeviceUseCase, rg *gin.RouterGroup, cfg *config.Config) *NearestDeviceController {
|
||||||
return &NearestDeviceController{
|
return &NearestDeviceController{
|
||||||
nearestDeviceUC: nearestDeviceUC,
|
nearestDeviceUC: nearestDeviceUC,
|
||||||
rg: rg,
|
rg: rg,
|
||||||
cfg: cfg,
|
cfg: cfg,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *NearestDeviceController) Route() {
|
func (c *NearestDeviceController) Route() {
|
||||||
nearestDevices := c.rg.Group("/nearest-devices")
|
nearestDevices := c.rg.Group("/nearest-devices")
|
||||||
nearestDevices.Use(middleware.ConditionalRequireAnyRole(c.cfg,"Teknisi", "Admin", "Super Admin"))
|
nearestDevices.Use(middleware.ConditionalRequireAnyRole(c.cfg, "Teknisi", "Admin", "Super Admin"))
|
||||||
{
|
{
|
||||||
nearestDevices.POST("/search", c.getNearestDevices)
|
nearestDevices.POST("/search", c.getNearestDevices)
|
||||||
nearestDevices.GET("/:id", c.getNearestDeviceByID)
|
nearestDevices.GET("/:id", c.getNearestDeviceByID)
|
||||||
|
|
||||||
nearestDevices.POST("/towers/search", c.getNearestTowers)
|
nearestDevices.POST("/towers/search", c.getNearestTowers)
|
||||||
nearestDevices.GET("/towers/:id", c.getNearestTowerByID)
|
nearestDevices.GET("/towers/:id", c.getNearestTowerByID)
|
||||||
}
|
|
||||||
|
nearestDevices.POST("/odp/search", c.getNearestODPDevices)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *NearestDeviceController) getNearestTowers(ctx *gin.Context) {
|
func (c *NearestDeviceController) getNearestTowers(ctx *gin.Context) {
|
||||||
var request req.NearestTowerDTO
|
var request req.NearestTowerDTO
|
||||||
if err := ctx.ShouldBindJSON(&request); err != nil {
|
if err := ctx.ShouldBindJSON(&request); err != nil {
|
||||||
common.ErrorResponses(ctx, http.StatusBadRequest, err.Error())
|
common.ErrorResponses(ctx, http.StatusBadRequest, err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
towers, err := c.nearestDeviceUC.GetNearestTowers(request)
|
towers, err := c.nearestDeviceUC.GetNearestTowers(request)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
common.ErrorResponses(ctx, http.StatusBadRequest, err.Error())
|
common.ErrorResponses(ctx, http.StatusBadRequest, err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
response := gin.H{
|
response := gin.H{
|
||||||
"towers": towers,
|
"towers": towers,
|
||||||
"total": len(towers),
|
"total": len(towers),
|
||||||
"search_params": gin.H{
|
"search_params": gin.H{
|
||||||
"latitude": request.Latitude,
|
"latitude": request.Latitude,
|
||||||
"longitude": request.Longitude,
|
"longitude": request.Longitude,
|
||||||
"radius": request.Radius,
|
"radius": request.Radius,
|
||||||
"province": request.Province,
|
"province": request.Province,
|
||||||
"city": request.City,
|
"city": request.City,
|
||||||
"district": request.District,
|
"district": request.District,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
common.SingleResponses(ctx, "Nearest towers retrieved successfully", response)
|
common.SingleResponses(ctx, "Nearest towers retrieved successfully", response)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *NearestDeviceController) getNearestTowerByID(ctx *gin.Context) {
|
func (c *NearestDeviceController) getNearestTowerByID(ctx *gin.Context) {
|
||||||
id := ctx.Param("id")
|
id := ctx.Param("id")
|
||||||
towerID, err := uuid.Parse(id)
|
towerID, err := uuid.Parse(id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
common.ErrorResponses(ctx, http.StatusBadRequest, "Invalid tower ID")
|
common.ErrorResponses(ctx, http.StatusBadRequest, "Invalid tower ID")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get user coordinates from query params
|
// Get user coordinates from query params
|
||||||
latStr := ctx.Query("lat")
|
latStr := ctx.Query("lat")
|
||||||
lngStr := ctx.Query("lng")
|
lngStr := ctx.Query("lng")
|
||||||
|
|
||||||
if latStr == "" || lngStr == "" {
|
|
||||||
common.ErrorResponses(ctx, http.StatusBadRequest, "User coordinates (lat, lng) are required")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
userLat, err := strconv.ParseFloat(latStr, 64)
|
if latStr == "" || lngStr == "" {
|
||||||
if err != nil {
|
common.ErrorResponses(ctx, http.StatusBadRequest, "User coordinates (lat, lng) are required")
|
||||||
common.ErrorResponses(ctx, http.StatusBadRequest, "Invalid latitude")
|
return
|
||||||
return
|
}
|
||||||
}
|
|
||||||
|
|
||||||
userLng, err := strconv.ParseFloat(lngStr, 64)
|
userLat, err := strconv.ParseFloat(latStr, 64)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
common.ErrorResponses(ctx, http.StatusBadRequest, "Invalid longitude")
|
common.ErrorResponses(ctx, http.StatusBadRequest, "Invalid latitude")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
tower, err := c.nearestDeviceUC.GetNearestTowerByID(towerID, userLat, userLng)
|
userLng, err := strconv.ParseFloat(lngStr, 64)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
common.ErrorResponses(ctx, http.StatusNotFound, err.Error())
|
common.ErrorResponses(ctx, http.StatusBadRequest, "Invalid longitude")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
common.SingleResponses(ctx, "Tower details retrieved successfully", tower)
|
tower, err := c.nearestDeviceUC.GetNearestTowerByID(towerID, userLat, userLng)
|
||||||
|
if err != nil {
|
||||||
|
common.ErrorResponses(ctx, http.StatusNotFound, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
common.SingleResponses(ctx, "Tower details retrieved successfully", tower)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *NearestDeviceController) getNearestDevices(ctx *gin.Context) {
|
func (c *NearestDeviceController) getNearestDevices(ctx *gin.Context) {
|
||||||
var request req.NearestDeviceDTO
|
var request req.NearestDeviceDTO
|
||||||
if err := ctx.ShouldBindJSON(&request); err != nil {
|
if err := ctx.ShouldBindJSON(&request); err != nil {
|
||||||
common.ErrorResponses(ctx, http.StatusBadRequest, err.Error())
|
common.ErrorResponses(ctx, http.StatusBadRequest, err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
devices, err := c.nearestDeviceUC.GetNearestDevices(request)
|
devices, err := c.nearestDeviceUC.GetNearestDevices(request)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
common.ErrorResponses(ctx, http.StatusBadRequest, err.Error())
|
common.ErrorResponses(ctx, http.StatusBadRequest, err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
response := gin.H{
|
response := gin.H{
|
||||||
"devices": devices,
|
"devices": devices,
|
||||||
"total": len(devices),
|
"total": len(devices),
|
||||||
"search_params": gin.H{
|
"search_params": gin.H{
|
||||||
"latitude": request.Latitude,
|
"latitude": request.Latitude,
|
||||||
"longitude": request.Longitude,
|
"longitude": request.Longitude,
|
||||||
"radius": request.Radius,
|
"radius": request.Radius,
|
||||||
"province": request.Province,
|
"province": request.Province,
|
||||||
"city": request.City,
|
"city": request.City,
|
||||||
"district": request.District,
|
"district": request.District,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
common.SingleResponses(ctx, "Nearest devices retrieved successfully", response)
|
common.SingleResponses(ctx, "Nearest devices retrieved successfully", response)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *NearestDeviceController) getNearestDeviceByID(ctx *gin.Context) {
|
func (c *NearestDeviceController) getNearestDeviceByID(ctx *gin.Context) {
|
||||||
id := ctx.Param("id")
|
id := ctx.Param("id")
|
||||||
deviceID, err := uuid.Parse(id)
|
deviceID, err := uuid.Parse(id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
common.ErrorResponses(ctx, http.StatusBadRequest, "Invalid device ID")
|
common.ErrorResponses(ctx, http.StatusBadRequest, "Invalid device ID")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get user coordinates from query params
|
// Get user coordinates from query params
|
||||||
latStr := ctx.Query("lat")
|
latStr := ctx.Query("lat")
|
||||||
lngStr := ctx.Query("lng")
|
lngStr := ctx.Query("lng")
|
||||||
|
|
||||||
if latStr == "" || lngStr == "" {
|
|
||||||
common.ErrorResponses(ctx, http.StatusBadRequest, "User coordinates (lat, lng) are required")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
userLat, err := strconv.ParseFloat(latStr, 64)
|
if latStr == "" || lngStr == "" {
|
||||||
if err != nil {
|
common.ErrorResponses(ctx, http.StatusBadRequest, "User coordinates (lat, lng) are required")
|
||||||
common.ErrorResponses(ctx, http.StatusBadRequest, "Invalid latitude")
|
return
|
||||||
return
|
}
|
||||||
}
|
|
||||||
|
|
||||||
userLng, err := strconv.ParseFloat(lngStr, 64)
|
userLat, err := strconv.ParseFloat(latStr, 64)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
common.ErrorResponses(ctx, http.StatusBadRequest, "Invalid longitude")
|
common.ErrorResponses(ctx, http.StatusBadRequest, "Invalid latitude")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
device, err := c.nearestDeviceUC.GetNearestDeviceByID(deviceID, userLat, userLng)
|
userLng, err := strconv.ParseFloat(lngStr, 64)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
common.ErrorResponses(ctx, http.StatusNotFound, err.Error())
|
common.ErrorResponses(ctx, http.StatusBadRequest, "Invalid longitude")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
common.SingleResponses(ctx, "Device details retrieved successfully", device)
|
device, err := c.nearestDeviceUC.GetNearestDeviceByID(deviceID, userLat, userLng)
|
||||||
}
|
if err != nil {
|
||||||
|
common.ErrorResponses(ctx, http.StatusNotFound, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
common.SingleResponses(ctx, "Device details retrieved successfully", device)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *NearestDeviceController) getNearestODPDevices(ctx *gin.Context) {
|
||||||
|
var request req.NearestDeviceDTO
|
||||||
|
if err := ctx.ShouldBindJSON(&request); err != nil {
|
||||||
|
common.ErrorResponses(ctx, http.StatusBadRequest, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
odpType := "ODP"
|
||||||
|
request.DeviceType = &odpType
|
||||||
|
|
||||||
|
devices, err := c.nearestDeviceUC.GetNearestDevices(request)
|
||||||
|
if err != nil {
|
||||||
|
common.ErrorResponses(ctx, http.StatusBadRequest, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
response := gin.H{
|
||||||
|
"devices": devices,
|
||||||
|
"total": len(devices),
|
||||||
|
"search_params": gin.H{
|
||||||
|
"latitude": request.Latitude,
|
||||||
|
"longitude": request.Longitude,
|
||||||
|
"radius": request.Radius,
|
||||||
|
"device_type": "ODP",
|
||||||
|
"province": request.Province,
|
||||||
|
"city": request.City,
|
||||||
|
"district": request.District,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
common.SingleResponses(ctx, "Nearest ODP devices retrieved successfully", response)
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,14 @@
|
||||||
|
# OVERVIEW
|
||||||
|
Provides domain entities, DTOs, and ORM mappings for NAM's data model.
|
||||||
|
|
||||||
|
# STRUCTURE
|
||||||
|
- dto/req: Request DTOs with validation tags for incoming data.
|
||||||
|
- dto/res: Response DTOs for outgoing data.
|
||||||
|
- entity: GORM-based domain entities located in model/entity/.
|
||||||
|
|
||||||
|
# CONVENTIONS
|
||||||
|
- UUID primary keys are defined with gorm:"type:uuid;primaryKey" on ID fields.
|
||||||
|
- DTOs in model/dto/req/ use validation tags (e.g., validate:"required", binding:"required") and JSON tags.
|
||||||
|
- Response DTOs in model/dto/res/ carry serialized data with appropriate JSON tags.
|
||||||
|
- Tables names are controlled by a TableName() string method on each entity.
|
||||||
|
- Entities are in model/entity/ with GORM tags; DTOs reflect input/output shapes for API boundaries.
|
||||||
|
|
@ -1,11 +1,12 @@
|
||||||
package req
|
package req
|
||||||
|
|
||||||
type NearestDeviceDTO struct {
|
type NearestDeviceDTO struct {
|
||||||
Longitude float64 `json:"longitude" validate:"required"`
|
Longitude float64 `json:"longitude" validate:"required"`
|
||||||
Latitude float64 `json:"latitude" validate:"required"`
|
Latitude float64 `json:"latitude" validate:"required"`
|
||||||
Radius float64 `json:"radius" validate:"omitempty,min=0.1,max=50"` // Default 5km, max 50km
|
Radius float64 `json:"radius" validate:"omitempty,min=0.1,max=50"` // Default 5km, max 50km
|
||||||
Limit int `json:"limit" validate:"omitempty,min=1,max=100"` // Default 10, max 100
|
Limit int `json:"limit" validate:"omitempty,min=1,max=100"` // Default 10, max 100
|
||||||
Province *string `json:"province,omitempty"`
|
Province *string `json:"province,omitempty"`
|
||||||
City *string `json:"city,omitempty"`
|
City *string `json:"city,omitempty"`
|
||||||
District *string `json:"district,omitempty"`
|
District *string `json:"district,omitempty"`
|
||||||
}
|
DeviceType *string `json:"device_type,omitempty"`
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,20 @@
|
||||||
|
## OVERVIEW
|
||||||
|
Repository layer for NAM with 13 repos using interface-driven design and GORM transactions.
|
||||||
|
|
||||||
|
## WHERE TO LOOK
|
||||||
|
- repository/ for all implementations
|
||||||
|
- Interface: {Entity}Repo, Constructor: New{Entity}Repo(db *gorm.DB)
|
||||||
|
- Transaction pattern: db.Transaction(func(tx *gorm.DB) error {...})
|
||||||
|
- Files: devices_repo.go, users_repo.go, tower_repo.go, olt_repo.go, etc.
|
||||||
|
|
||||||
|
## CONVENTIONS
|
||||||
|
- Interface naming: {Entity}Repo
|
||||||
|
- Constructor: New{Entity}Repo(db *gorm.DB) {Entity}Repo
|
||||||
|
- Return (data, error) pairs
|
||||||
|
- Use wrapped errors for context
|
||||||
|
|
||||||
|
## ANTI-PATTERNS
|
||||||
|
- Skip transactions for mutating operations
|
||||||
|
- Return nil error on failures
|
||||||
|
- Embed business logic in repos
|
||||||
|
- Instantiate DB connections inside repos
|
||||||
|
|
@ -1,159 +1,158 @@
|
||||||
package repository
|
package repository
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"users_management/m/model/entity"
|
"github.com/google/uuid"
|
||||||
"github.com/google/uuid"
|
"gorm.io/gorm"
|
||||||
"gorm.io/gorm"
|
"users_management/m/model/entity"
|
||||||
)
|
)
|
||||||
|
|
||||||
type NearestDeviceRepo interface {
|
type NearestDeviceRepo interface {
|
||||||
GetNearestDevices(longitude, latitude, radius float64, limit int, province, city, district *string) ([]entity.DeviceWithDistance, error)
|
GetNearestDevices(longitude, latitude, radius float64, limit int, province, city, district, deviceType *string) ([]entity.DeviceWithDistance, error)
|
||||||
GetDeviceByIDWithConnections(id uuid.UUID) (entity.Device, error)
|
GetDeviceByIDWithConnections(id uuid.UUID) (entity.Device, error)
|
||||||
GetBackbonesByDeviceID(deviceID uuid.UUID) ([]entity.Backbone, error)
|
GetBackbonesByDeviceID(deviceID uuid.UUID) ([]entity.Backbone, error)
|
||||||
GetFishbonesByDeviceID(deviceID uuid.UUID) ([]entity.Fishbone, error)
|
GetFishbonesByDeviceID(deviceID uuid.UUID) ([]entity.Fishbone, error)
|
||||||
GetTowersByDeviceID(deviceID uuid.UUID) ([]entity.Tower, error)
|
GetTowersByDeviceID(deviceID uuid.UUID) ([]entity.Tower, error)
|
||||||
CountConnectionsByDeviceID(deviceID uuid.UUID) (backboneCount, fishboneCount, towerCount int, err error)
|
CountConnectionsByDeviceID(deviceID uuid.UUID) (backboneCount, fishboneCount, towerCount int, err error)
|
||||||
GetNearestTowers(longitude, latitude, radius float64, limit int, province, city, district *string) ([]entity.TowerWithDistance, error)
|
GetNearestTowers(longitude, latitude, radius float64, limit int, province, city, district *string) ([]entity.TowerWithDistance, error)
|
||||||
GetTowerByIDWithConnections(id uuid.UUID) (entity.Tower, error)
|
GetTowerByIDWithConnections(id uuid.UUID) (entity.Tower, error)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type nearestDeviceRepo struct {
|
type nearestDeviceRepo struct {
|
||||||
db *gorm.DB
|
db *gorm.DB
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewNearestDeviceRepo(db *gorm.DB) NearestDeviceRepo {
|
func NewNearestDeviceRepo(db *gorm.DB) NearestDeviceRepo {
|
||||||
return &nearestDeviceRepo{
|
return &nearestDeviceRepo{
|
||||||
db: db,
|
db: db,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *nearestDeviceRepo) GetNearestTowers(longitude, latitude, radius float64, limit int, province, city, district *string) ([]entity.TowerWithDistance, error) {
|
func (r *nearestDeviceRepo) GetNearestTowers(longitude, latitude, radius float64, limit int, province, city, district *string) ([]entity.TowerWithDistance, error) {
|
||||||
var towers []entity.TowerWithDistance
|
var towers []entity.TowerWithDistance
|
||||||
|
|
||||||
// Build the subquery first - join with devices to get location filters
|
// Build the subquery first - join with devices to get location filters
|
||||||
subQuery := r.db.Table("towers").
|
subQuery := r.db.Table("towers").
|
||||||
Select(`towers.id, towers.tower_code, towers.longitude, towers.latitude,
|
Select(`towers.id, towers.tower_code, towers.longitude, towers.latitude,
|
||||||
towers.image_url, towers.external_tower, towers.dev_id, towers.created_at, towers.updated_at,
|
towers.image_url, towers.external_tower, towers.dev_id, towers.created_at, towers.updated_at,
|
||||||
devices.device_code, devices.province, devices.city, devices.district,
|
devices.device_code, devices.province, devices.city, devices.district,
|
||||||
(6371 * acos(cos(radians(?)) * cos(radians(towers.latitude)) * cos(radians(towers.longitude) - radians(?)) + sin(radians(?)) * sin(radians(towers.latitude)))) AS distance`,
|
(6371 * acos(cos(radians(?)) * cos(radians(towers.latitude)) * cos(radians(towers.longitude) - radians(?)) + sin(radians(?)) * sin(radians(towers.latitude)))) AS distance`,
|
||||||
latitude, longitude, latitude).
|
latitude, longitude, latitude).
|
||||||
Joins("LEFT JOIN devices ON towers.dev_id = devices.id")
|
Joins("LEFT JOIN devices ON towers.dev_id = devices.id")
|
||||||
|
|
||||||
// Apply location filters to subquery
|
// Apply location filters to subquery
|
||||||
if province != nil && *province != "" {
|
if province != nil && *province != "" {
|
||||||
subQuery = subQuery.Where("devices.province = ?", *province)
|
subQuery = subQuery.Where("devices.province = ?", *province)
|
||||||
}
|
}
|
||||||
if city != nil && *city != "" {
|
if city != nil && *city != "" {
|
||||||
subQuery = subQuery.Where("devices.city = ?", *city)
|
subQuery = subQuery.Where("devices.city = ?", *city)
|
||||||
}
|
}
|
||||||
if district != nil && *district != "" {
|
if district != nil && *district != "" {
|
||||||
subQuery = subQuery.Where("devices.district = ?", *district)
|
subQuery = subQuery.Where("devices.district = ?", *district)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Use the subquery in the main query
|
// Use the subquery in the main query
|
||||||
err := r.db.Table("(?) as towers_with_distance", subQuery).
|
err := r.db.Table("(?) as towers_with_distance", subQuery).
|
||||||
Where("distance <= ?", radius).
|
Where("distance <= ?", radius).
|
||||||
Order("distance ASC").
|
Order("distance ASC").
|
||||||
Limit(limit).
|
Limit(limit).
|
||||||
Scan(&towers).Error
|
Scan(&towers).Error
|
||||||
|
|
||||||
return towers, err
|
return towers, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *nearestDeviceRepo) GetTowerByIDWithConnections(id uuid.UUID) (entity.Tower, error) {
|
func (r *nearestDeviceRepo) GetTowerByIDWithConnections(id uuid.UUID) (entity.Tower, error) {
|
||||||
var tower entity.Tower
|
var tower entity.Tower
|
||||||
err := r.db.Preload("Device").Where("id = ?", id).First(&tower).Error
|
err := r.db.Preload("Device").Where("id = ?", id).First(&tower).Error
|
||||||
return tower, err
|
return tower, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *nearestDeviceRepo) GetNearestDevices(longitude, latitude, radius float64, limit int, province, city, district *string) ([]entity.DeviceWithDistance, error) {
|
func (r *nearestDeviceRepo) GetNearestDevices(longitude, latitude, radius float64, limit int, province, city, district, deviceType *string) ([]entity.DeviceWithDistance, error) {
|
||||||
var devices []entity.DeviceWithDistance
|
var devices []entity.DeviceWithDistance
|
||||||
|
|
||||||
// Build the subquery first
|
subQuery := r.db.Table("devices").
|
||||||
subQuery := r.db.Table("devices").
|
Select(`id, device_code, device_type, longitude, latitude, port_amount, status,
|
||||||
Select(`id, device_code, device_type, longitude, latitude, port_amount, status,
|
|
||||||
region, province, city, district, image_url, created_at, updated_at,
|
region, province, city, district, image_url, created_at, updated_at,
|
||||||
(6371 * acos(cos(radians(?)) * cos(radians(latitude)) * cos(radians(longitude) - radians(?)) + sin(radians(?)) * sin(radians(latitude)))) AS distance`,
|
(6371 * acos(cos(radians(?)) * cos(radians(latitude)) * cos(radians(longitude) - radians(?)) + sin(radians(?)) * sin(radians(latitude)))) AS distance`,
|
||||||
latitude, longitude, latitude)
|
latitude, longitude, latitude)
|
||||||
|
|
||||||
// Apply location filters to subquery
|
if province != nil && *province != "" {
|
||||||
if province != nil && *province != "" {
|
subQuery = subQuery.Where("province = ?", *province)
|
||||||
subQuery = subQuery.Where("province = ?", *province)
|
}
|
||||||
}
|
if city != nil && *city != "" {
|
||||||
if city != nil && *city != "" {
|
subQuery = subQuery.Where("city = ?", *city)
|
||||||
subQuery = subQuery.Where("city = ?", *city)
|
}
|
||||||
}
|
if district != nil && *district != "" {
|
||||||
if district != nil && *district != "" {
|
subQuery = subQuery.Where("district = ?", *district)
|
||||||
subQuery = subQuery.Where("district = ?", *district)
|
}
|
||||||
}
|
if deviceType != nil && *deviceType != "" {
|
||||||
|
subQuery = subQuery.Where("device_type = ?", *deviceType)
|
||||||
// Use the subquery in the main query
|
}
|
||||||
err := r.db.Table("(?) as devices_with_distance", subQuery).
|
|
||||||
Where("distance <= ?", radius).
|
err := r.db.Table("(?) as devices_with_distance", subQuery).
|
||||||
Order("distance ASC").
|
Where("distance <= ?", radius).
|
||||||
Limit(limit).
|
Order("distance ASC").
|
||||||
Scan(&devices).Error
|
Limit(limit).
|
||||||
|
Scan(&devices).Error
|
||||||
return devices, err
|
|
||||||
|
return devices, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *nearestDeviceRepo) GetDeviceByIDWithConnections(id uuid.UUID) (entity.Device, error) {
|
func (r *nearestDeviceRepo) GetDeviceByIDWithConnections(id uuid.UUID) (entity.Device, error) {
|
||||||
var device entity.Device
|
var device entity.Device
|
||||||
err := r.db.Where("id = ?", id).First(&device).Error
|
err := r.db.Where("id = ?", id).First(&device).Error
|
||||||
return device, err
|
return device, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *nearestDeviceRepo) GetBackbonesByDeviceID(deviceID uuid.UUID) ([]entity.Backbone, error) {
|
func (r *nearestDeviceRepo) GetBackbonesByDeviceID(deviceID uuid.UUID) ([]entity.Backbone, error) {
|
||||||
var backbones []entity.Backbone
|
var backbones []entity.Backbone
|
||||||
err := r.db.Preload("DeviceStart").Preload("DeviceEnd").
|
err := r.db.Preload("DeviceStart").Preload("DeviceEnd").
|
||||||
Where("dev_start_id = ? OR dev_end_id = ?", deviceID, deviceID).
|
Where("dev_start_id = ? OR dev_end_id = ?", deviceID, deviceID).
|
||||||
Find(&backbones).Error
|
Find(&backbones).Error
|
||||||
return backbones, err
|
return backbones, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *nearestDeviceRepo) GetFishbonesByDeviceID(deviceID uuid.UUID) ([]entity.Fishbone, error) {
|
func (r *nearestDeviceRepo) GetFishbonesByDeviceID(deviceID uuid.UUID) ([]entity.Fishbone, error) {
|
||||||
var fishbones []entity.Fishbone
|
var fishbones []entity.Fishbone
|
||||||
err := r.db.Preload("Backbone").Preload("DeviceStart").Preload("DeviceEnd").
|
err := r.db.Preload("Backbone").Preload("DeviceStart").Preload("DeviceEnd").
|
||||||
Where("dev_start_id = ? OR dev_end_id = ?", deviceID, deviceID).
|
Where("dev_start_id = ? OR dev_end_id = ?", deviceID, deviceID).
|
||||||
Find(&fishbones).Error
|
Find(&fishbones).Error
|
||||||
return fishbones, err
|
return fishbones, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *nearestDeviceRepo) GetTowersByDeviceID(deviceID uuid.UUID) ([]entity.Tower, error) {
|
func (r *nearestDeviceRepo) GetTowersByDeviceID(deviceID uuid.UUID) ([]entity.Tower, error) {
|
||||||
var towers []entity.Tower
|
var towers []entity.Tower
|
||||||
err := r.db.Preload("Device").
|
err := r.db.Preload("Device").
|
||||||
Where("dev_id = ?", deviceID).
|
Where("dev_id = ?", deviceID).
|
||||||
Find(&towers).Error
|
Find(&towers).Error
|
||||||
return towers, err
|
return towers, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *nearestDeviceRepo) CountConnectionsByDeviceID(deviceID uuid.UUID) (backboneCount, fishboneCount, towerCount int, err error) {
|
func (r *nearestDeviceRepo) CountConnectionsByDeviceID(deviceID uuid.UUID) (backboneCount, fishboneCount, towerCount int, err error) {
|
||||||
var backboneCountInt64, fishboneCountInt64, towerCountInt64 int64
|
var backboneCountInt64, fishboneCountInt64, towerCountInt64 int64
|
||||||
|
|
||||||
// Count backbones
|
// Count backbones
|
||||||
err = r.db.Model(&entity.Backbone{}).
|
err = r.db.Model(&entity.Backbone{}).
|
||||||
Where("dev_start_id = ? OR dev_end_id = ?", deviceID, deviceID).
|
Where("dev_start_id = ? OR dev_end_id = ?", deviceID, deviceID).
|
||||||
Count(&backboneCountInt64).Error
|
Count(&backboneCountInt64).Error
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, 0, 0, err
|
return 0, 0, 0, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Count fishbones
|
// Count fishbones
|
||||||
err = r.db.Model(&entity.Fishbone{}).
|
err = r.db.Model(&entity.Fishbone{}).
|
||||||
Where("dev_start_id = ? OR dev_end_id = ?", deviceID, deviceID).
|
Where("dev_start_id = ? OR dev_end_id = ?", deviceID, deviceID).
|
||||||
Count(&fishboneCountInt64).Error
|
Count(&fishboneCountInt64).Error
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, 0, 0, err
|
return 0, 0, 0, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Count towers
|
// Count towers
|
||||||
err = r.db.Model(&entity.Tower{}).
|
err = r.db.Model(&entity.Tower{}).
|
||||||
Where("dev_id = ?", deviceID).
|
Where("dev_id = ?", deviceID).
|
||||||
Count(&towerCountInt64).Error
|
Count(&towerCountInt64).Error
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, 0, 0, err
|
return 0, 0, 0, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return int(backboneCountInt64), int(fishboneCountInt64), int(towerCountInt64), nil
|
return int(backboneCountInt64), int(fishboneCountInt64), int(towerCountInt64), nil
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,24 @@
|
||||||
|
OVERVIEW
|
||||||
|
Scaffold for 14 high‑complexity use cases with strict interfaces and DI.
|
||||||
|
|
||||||
|
WHERE TO LOOK
|
||||||
|
- usecase/ directory for interfaces and New{Entity}UseCase constructors
|
||||||
|
- repository/ for repository interfaces consumed by use cases
|
||||||
|
- model/ for DTOs and domain entities used by use cases
|
||||||
|
- manager/ for DI wiring examples (injecting GeocodingService)
|
||||||
|
- config/ or utils/ for validator setup references invoked by use cases
|
||||||
|
|
||||||
|
CONVENTIONS
|
||||||
|
- Use case interfaces define all methods for an entity
|
||||||
|
- Constructor: New{Entity}UseCase(repo, geocoder) returns {Entity}UseCase interface
|
||||||
|
- Validation uses validator.New() inside use cases
|
||||||
|
- GeocodingService is injected via dependency injection (not instantiated in use cases)
|
||||||
|
- File naming uses snake_case; interfaces named {Entity}UseCase / {Entity}Repo
|
||||||
|
- Tests follow existing project patterns and naming
|
||||||
|
|
||||||
|
ANTI-PATTERNS
|
||||||
|
- Do not skip validation in use cases; always validate inputs via validator
|
||||||
|
- Do not couple to concrete repositories; depend on interfaces only
|
||||||
|
- Do not instantiate GeocodingService inside use cases; rely on DI
|
||||||
|
- Do not mix business logic into delivery layer
|
||||||
|
- Do not bypass existing patterns for separation of concerns (use cases separate from controllers)
|
||||||
|
|
@ -1,163 +1,164 @@
|
||||||
package usecase
|
package usecase
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"math"
|
"math"
|
||||||
"users_management/m/model/dto/req"
|
"users_management/m/model/dto/req"
|
||||||
"users_management/m/model/dto/res"
|
"users_management/m/model/dto/res"
|
||||||
"users_management/m/repository"
|
"users_management/m/repository"
|
||||||
"users_management/m/utils/helper"
|
"users_management/m/utils/helper"
|
||||||
"users_management/m/utils/service"
|
"users_management/m/utils/service"
|
||||||
|
|
||||||
"github.com/go-playground/validator/v10"
|
"github.com/go-playground/validator/v10"
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
)
|
)
|
||||||
|
|
||||||
type NearestDeviceUseCase interface {
|
type NearestDeviceUseCase interface {
|
||||||
GetNearestDevices(request req.NearestDeviceDTO) ([]res.NearestDeviceResponse, error)
|
GetNearestDevices(request req.NearestDeviceDTO) ([]res.NearestDeviceResponse, error)
|
||||||
GetNearestDeviceByID(id uuid.UUID, userLat, userLng float64) (res.NearestDeviceDetailResponse, error)
|
GetNearestDeviceByID(id uuid.UUID, userLat, userLng float64) (res.NearestDeviceDetailResponse, error)
|
||||||
GetNearestTowers(request req.NearestTowerDTO) ([]res.NearestTowerResponse, error)
|
GetNearestTowers(request req.NearestTowerDTO) ([]res.NearestTowerResponse, error)
|
||||||
GetNearestTowerByID(id uuid.UUID, userLat, userLng float64) (res.NearestTowerDetailResponse, error)
|
GetNearestTowerByID(id uuid.UUID, userLat, userLng float64) (res.NearestTowerDetailResponse, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
type nearestDeviceUseCase struct {
|
type nearestDeviceUseCase struct {
|
||||||
nearestDeviceRepo repository.NearestDeviceRepo
|
nearestDeviceRepo repository.NearestDeviceRepo
|
||||||
geocoder service.GeocodingService
|
geocoder service.GeocodingService
|
||||||
validate *validator.Validate
|
validate *validator.Validate
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewNearestDeviceUseCase(nearestDeviceRepo repository.NearestDeviceRepo, geocoder service.GeocodingService) NearestDeviceUseCase {
|
func NewNearestDeviceUseCase(nearestDeviceRepo repository.NearestDeviceRepo, geocoder service.GeocodingService) NearestDeviceUseCase {
|
||||||
return &nearestDeviceUseCase{
|
return &nearestDeviceUseCase{
|
||||||
nearestDeviceRepo: nearestDeviceRepo,
|
nearestDeviceRepo: nearestDeviceRepo,
|
||||||
geocoder: geocoder,
|
geocoder: geocoder,
|
||||||
validate: validator.New(),
|
validate: validator.New(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u *nearestDeviceUseCase) GetNearestTowers(request req.NearestTowerDTO) ([]res.NearestTowerResponse, error) {
|
func (u *nearestDeviceUseCase) GetNearestTowers(request req.NearestTowerDTO) ([]res.NearestTowerResponse, error) {
|
||||||
err := u.validate.Struct(request)
|
err := u.validate.Struct(request)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set defaults
|
// Set defaults
|
||||||
radius := request.Radius
|
radius := request.Radius
|
||||||
if radius == 0 {
|
if radius == 0 {
|
||||||
radius = 5.0 // Default 5km
|
radius = 5.0 // Default 5km
|
||||||
}
|
}
|
||||||
|
|
||||||
limit := request.Limit
|
limit := request.Limit
|
||||||
if limit == 0 {
|
if limit == 0 {
|
||||||
limit = 10 // Default 10 towers
|
limit = 10 // Default 10 towers
|
||||||
}
|
}
|
||||||
|
|
||||||
towers, err := u.nearestDeviceRepo.GetNearestTowers(
|
towers, err := u.nearestDeviceRepo.GetNearestTowers(
|
||||||
request.Longitude,
|
request.Longitude,
|
||||||
request.Latitude,
|
request.Latitude,
|
||||||
radius,
|
radius,
|
||||||
limit,
|
limit,
|
||||||
request.Province,
|
request.Province,
|
||||||
request.City,
|
request.City,
|
||||||
request.District,
|
request.District,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
responses, err := helper.ConvertToNearestTowerResponses(towers, u.geocoder)
|
responses, err := helper.ConvertToNearestTowerResponses(towers, u.geocoder)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return responses, nil
|
return responses, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u *nearestDeviceUseCase) GetNearestTowerByID(id uuid.UUID, userLat, userLng float64) (res.NearestTowerDetailResponse, error) {
|
func (u *nearestDeviceUseCase) GetNearestTowerByID(id uuid.UUID, userLat, userLng float64) (res.NearestTowerDetailResponse, error) {
|
||||||
tower, err := u.nearestDeviceRepo.GetTowerByIDWithConnections(id)
|
tower, err := u.nearestDeviceRepo.GetTowerByIDWithConnections(id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return res.NearestTowerDetailResponse{}, err
|
return res.NearestTowerDetailResponse{}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Calculate distance
|
// Calculate distance
|
||||||
distance := calculateDistance(userLat, userLng, tower.Latitude, tower.Longitude)
|
distance := calculateDistance(userLat, userLng, tower.Latitude, tower.Longitude)
|
||||||
|
|
||||||
response, err := helper.ConvertToNearestTowerDetailResponse(tower, distance, u.nearestDeviceRepo, u.geocoder)
|
response, err := helper.ConvertToNearestTowerDetailResponse(tower, distance, u.nearestDeviceRepo, u.geocoder)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return res.NearestTowerDetailResponse{}, err
|
return res.NearestTowerDetailResponse{}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return response, nil
|
return response, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u *nearestDeviceUseCase) GetNearestDevices(request req.NearestDeviceDTO) ([]res.NearestDeviceResponse, error) {
|
func (u *nearestDeviceUseCase) GetNearestDevices(request req.NearestDeviceDTO) ([]res.NearestDeviceResponse, error) {
|
||||||
err := u.validate.Struct(request)
|
err := u.validate.Struct(request)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set defaults
|
// Set defaults
|
||||||
radius := request.Radius
|
radius := request.Radius
|
||||||
if radius == 0 {
|
if radius == 0 {
|
||||||
radius = 5.0 // Default 5km
|
radius = 5.0 // Default 5km
|
||||||
}
|
}
|
||||||
|
|
||||||
limit := request.Limit
|
limit := request.Limit
|
||||||
if limit == 0 {
|
if limit == 0 {
|
||||||
limit = 10 // Default 10 devices
|
limit = 10 // Default 10 devices
|
||||||
}
|
}
|
||||||
|
|
||||||
devices, err := u.nearestDeviceRepo.GetNearestDevices(
|
devices, err := u.nearestDeviceRepo.GetNearestDevices(
|
||||||
request.Longitude,
|
request.Longitude,
|
||||||
request.Latitude,
|
request.Latitude,
|
||||||
radius,
|
radius,
|
||||||
limit,
|
limit,
|
||||||
request.Province,
|
request.Province,
|
||||||
request.City,
|
request.City,
|
||||||
request.District,
|
request.District,
|
||||||
)
|
request.DeviceType,
|
||||||
if err != nil {
|
)
|
||||||
return nil, err
|
if err != nil {
|
||||||
}
|
return nil, err
|
||||||
|
}
|
||||||
// Updated function call - removed userLat, userLng parameters since distance is already calculated
|
|
||||||
responses, err := helper.ConvertToNearestDeviceResponses(devices, u.nearestDeviceRepo, u.geocoder)
|
// Updated function call - removed userLat, userLng parameters since distance is already calculated
|
||||||
if err != nil {
|
responses, err := helper.ConvertToNearestDeviceResponses(devices, u.nearestDeviceRepo, u.geocoder)
|
||||||
return nil, err
|
if err != nil {
|
||||||
}
|
return nil, err
|
||||||
|
}
|
||||||
return responses, nil
|
|
||||||
|
return responses, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u *nearestDeviceUseCase) GetNearestDeviceByID(id uuid.UUID, userLat, userLng float64) (res.NearestDeviceDetailResponse, error) {
|
func (u *nearestDeviceUseCase) GetNearestDeviceByID(id uuid.UUID, userLat, userLng float64) (res.NearestDeviceDetailResponse, error) {
|
||||||
device, err := u.nearestDeviceRepo.GetDeviceByIDWithConnections(id)
|
device, err := u.nearestDeviceRepo.GetDeviceByIDWithConnections(id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return res.NearestDeviceDetailResponse{}, err
|
return res.NearestDeviceDetailResponse{}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Calculate distance
|
// Calculate distance
|
||||||
distance := calculateDistance(userLat, userLng, device.Latitude, device.Longitude)
|
distance := calculateDistance(userLat, userLng, device.Latitude, device.Longitude)
|
||||||
|
|
||||||
response, err := helper.ConvertToNearestDeviceDetailResponse(device, distance, u.nearestDeviceRepo, u.geocoder)
|
response, err := helper.ConvertToNearestDeviceDetailResponse(device, distance, u.nearestDeviceRepo, u.geocoder)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return res.NearestDeviceDetailResponse{}, err
|
return res.NearestDeviceDetailResponse{}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return response, nil
|
return response, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// calculateDistance calculates the distance between two coordinates using Haversine formula
|
// calculateDistance calculates the distance between two coordinates using Haversine formula
|
||||||
func calculateDistance(lat1, lng1, lat2, lng2 float64) float64 {
|
func calculateDistance(lat1, lng1, lat2, lng2 float64) float64 {
|
||||||
const earthRadius = 6371 // Earth's radius in kilometers
|
const earthRadius = 6371 // Earth's radius in kilometers
|
||||||
|
|
||||||
lat1Rad := lat1 * math.Pi / 180
|
lat1Rad := lat1 * math.Pi / 180
|
||||||
lng1Rad := lng1 * math.Pi / 180
|
lng1Rad := lng1 * math.Pi / 180
|
||||||
lat2Rad := lat2 * math.Pi / 180
|
lat2Rad := lat2 * math.Pi / 180
|
||||||
lng2Rad := lng2 * math.Pi / 180
|
lng2Rad := lng2 * math.Pi / 180
|
||||||
|
|
||||||
dlat := lat2Rad - lat1Rad
|
dlat := lat2Rad - lat1Rad
|
||||||
dlng := lng2Rad - lng1Rad
|
dlng := lng2Rad - lng1Rad
|
||||||
|
|
||||||
a := math.Sin(dlat/2)*math.Sin(dlat/2) + math.Cos(lat1Rad)*math.Cos(lat2Rad)*math.Sin(dlng/2)*math.Sin(dlng/2)
|
a := math.Sin(dlat/2)*math.Sin(dlat/2) + math.Cos(lat1Rad)*math.Cos(lat2Rad)*math.Sin(dlng/2)*math.Sin(dlng/2)
|
||||||
c := 2 * math.Atan2(math.Sqrt(a), math.Sqrt(1-a))
|
c := 2 * math.Atan2(math.Sqrt(a), math.Sqrt(1-a))
|
||||||
|
|
||||||
return earthRadius * c
|
return earthRadius * c
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,381 @@
|
||||||
|
package usecase
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
"users_management/m/model/dto/req"
|
||||||
|
"users_management/m/model/entity"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
type mockNearestDeviceRepo struct {
|
||||||
|
GetNearestDevicesFunc func(longitude, latitude, radius float64, limit int, province, city, district, deviceType *string) ([]entity.DeviceWithDistance, error)
|
||||||
|
GetDeviceByIDWithConnectionsFunc func(id uuid.UUID) (entity.Device, error)
|
||||||
|
CountConnectionsByDeviceIDFunc func(deviceID uuid.UUID) (int, int, int, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mockNearestDeviceRepo) GetNearestDevices(longitude, latitude, radius float64, limit int, province, city, district, deviceType *string) ([]entity.DeviceWithDistance, error) {
|
||||||
|
if m.GetNearestDevicesFunc != nil {
|
||||||
|
return m.GetNearestDevicesFunc(longitude, latitude, radius, limit, province, city, district, deviceType)
|
||||||
|
}
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mockNearestDeviceRepo) GetDeviceByIDWithConnections(id uuid.UUID) (entity.Device, error) {
|
||||||
|
if m.GetDeviceByIDWithConnectionsFunc != nil {
|
||||||
|
return m.GetDeviceByIDWithConnectionsFunc(id)
|
||||||
|
}
|
||||||
|
return entity.Device{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mockNearestDeviceRepo) GetBackbonesByDeviceID(deviceID uuid.UUID) ([]entity.Backbone, error) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mockNearestDeviceRepo) GetFishbonesByDeviceID(deviceID uuid.UUID) ([]entity.Fishbone, error) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mockNearestDeviceRepo) GetTowersByDeviceID(deviceID uuid.UUID) ([]entity.Tower, error) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mockNearestDeviceRepo) CountConnectionsByDeviceID(deviceID uuid.UUID) (backboneCount, fishboneCount, towerCount int, err error) {
|
||||||
|
if m.CountConnectionsByDeviceIDFunc != nil {
|
||||||
|
return m.CountConnectionsByDeviceIDFunc(deviceID)
|
||||||
|
}
|
||||||
|
return 0, 0, 0, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mockNearestDeviceRepo) GetNearestTowers(longitude, latitude, radius float64, limit int, province, city, district *string) ([]entity.TowerWithDistance, error) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mockNearestDeviceRepo) GetTowerByIDWithConnections(id uuid.UUID) (entity.Tower, error) {
|
||||||
|
return entity.Tower{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type mockGeocoder struct{}
|
||||||
|
|
||||||
|
func (m *mockGeocoder) GetAddressFromCoordinates(lat, lng float64) (string, error) {
|
||||||
|
return "Mock Address, City", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetNearestDevices_Success(t *testing.T) {
|
||||||
|
deviceID := uuid.New()
|
||||||
|
provinceStr := "Jakarta"
|
||||||
|
cityStr := "Jakarta Selatan"
|
||||||
|
|
||||||
|
mockRepo := &mockNearestDeviceRepo{
|
||||||
|
GetNearestDevicesFunc: func(longitude, latitude, radius float64, limit int, prov, cty, district, deviceType *string) ([]entity.DeviceWithDistance, error) {
|
||||||
|
if longitude != 106.8 {
|
||||||
|
t.Errorf("expected longitude 106.8, got %f", longitude)
|
||||||
|
}
|
||||||
|
if latitude != -6.2 {
|
||||||
|
t.Errorf("expected latitude -6.2, got %f", latitude)
|
||||||
|
}
|
||||||
|
if radius != 5.0 {
|
||||||
|
t.Errorf("expected radius 5.0, got %f", radius)
|
||||||
|
}
|
||||||
|
if limit != 10 {
|
||||||
|
t.Errorf("expected limit 10, got %d", limit)
|
||||||
|
}
|
||||||
|
_ = prov
|
||||||
|
_ = cty
|
||||||
|
return []entity.DeviceWithDistance{
|
||||||
|
{
|
||||||
|
ID: deviceID,
|
||||||
|
DeviceCode: "ODP-001",
|
||||||
|
DeviceType: entity.ODP,
|
||||||
|
Longitude: 106.81,
|
||||||
|
Latitude: -6.21,
|
||||||
|
PortAmount: 8,
|
||||||
|
Status: entity.ActiveDev,
|
||||||
|
Province: &provinceStr,
|
||||||
|
City: &cityStr,
|
||||||
|
Distance: 1.5,
|
||||||
|
CreatedAt: time.Now(),
|
||||||
|
UpdatedAt: time.Now(),
|
||||||
|
},
|
||||||
|
}, nil
|
||||||
|
},
|
||||||
|
CountConnectionsByDeviceIDFunc: func(deviceID uuid.UUID) (int, int, int, error) {
|
||||||
|
return 2, 3, 1, nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
uc := NewNearestDeviceUseCase(mockRepo, &mockGeocoder{})
|
||||||
|
|
||||||
|
request := req.NearestDeviceDTO{
|
||||||
|
Longitude: 106.8,
|
||||||
|
Latitude: -6.2,
|
||||||
|
Radius: 0,
|
||||||
|
Limit: 0,
|
||||||
|
}
|
||||||
|
|
||||||
|
devices, err := uc.GetNearestDevices(request)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("expected no error, got %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(devices) != 1 {
|
||||||
|
t.Fatalf("expected 1 device, got %d", len(devices))
|
||||||
|
}
|
||||||
|
|
||||||
|
if devices[0].DeviceCode != "ODP-001" {
|
||||||
|
t.Errorf("expected device code ODP-001, got %s", devices[0].DeviceCode)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetNearestDevices_WithDeviceTypeFilter(t *testing.T) {
|
||||||
|
deviceType := "ODP"
|
||||||
|
|
||||||
|
mockRepo := &mockNearestDeviceRepo{
|
||||||
|
GetNearestDevicesFunc: func(longitude, latitude, radius float64, limit int, province, city, district, deviceType *string) ([]entity.DeviceWithDistance, error) {
|
||||||
|
if deviceType == nil || *deviceType != "ODP" {
|
||||||
|
t.Errorf("expected deviceType filter to be ODP")
|
||||||
|
}
|
||||||
|
return []entity.DeviceWithDistance{
|
||||||
|
{
|
||||||
|
ID: uuid.New(),
|
||||||
|
DeviceCode: "ODP-002",
|
||||||
|
DeviceType: entity.ODP,
|
||||||
|
Longitude: 106.82,
|
||||||
|
Latitude: -6.22,
|
||||||
|
PortAmount: 16,
|
||||||
|
Status: entity.ActiveDev,
|
||||||
|
Distance: 2.5,
|
||||||
|
CreatedAt: time.Now(),
|
||||||
|
UpdatedAt: time.Now(),
|
||||||
|
},
|
||||||
|
}, nil
|
||||||
|
},
|
||||||
|
CountConnectionsByDeviceIDFunc: func(deviceID uuid.UUID) (int, int, int, error) {
|
||||||
|
return 1, 2, 0, nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
uc := NewNearestDeviceUseCase(mockRepo, &mockGeocoder{})
|
||||||
|
|
||||||
|
request := req.NearestDeviceDTO{
|
||||||
|
Longitude: 106.8,
|
||||||
|
Latitude: -6.2,
|
||||||
|
Radius: 5.0,
|
||||||
|
Limit: 10,
|
||||||
|
DeviceType: &deviceType,
|
||||||
|
}
|
||||||
|
|
||||||
|
devices, err := uc.GetNearestDevices(request)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("expected no error, got %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(devices) != 1 {
|
||||||
|
t.Fatalf("expected 1 device, got %d", len(devices))
|
||||||
|
}
|
||||||
|
|
||||||
|
if devices[0].DeviceType != "ODP" {
|
||||||
|
t.Errorf("expected device type ODP, got %s", devices[0].DeviceType)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetNearestDevices_RepositoryError(t *testing.T) {
|
||||||
|
mockRepo := &mockNearestDeviceRepo{
|
||||||
|
GetNearestDevicesFunc: func(longitude, latitude, radius float64, limit int, province, city, district, deviceType *string) ([]entity.DeviceWithDistance, error) {
|
||||||
|
return nil, errors.New("database connection failed")
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
uc := NewNearestDeviceUseCase(mockRepo, &mockGeocoder{})
|
||||||
|
|
||||||
|
request := req.NearestDeviceDTO{
|
||||||
|
Longitude: 106.8,
|
||||||
|
Latitude: -6.2,
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := uc.GetNearestDevices(request)
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("expected error, got nil")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetNearestDevices_EmptyResult(t *testing.T) {
|
||||||
|
mockRepo := &mockNearestDeviceRepo{
|
||||||
|
GetNearestDevicesFunc: func(longitude, latitude, radius float64, limit int, province, city, district, deviceType *string) ([]entity.DeviceWithDistance, error) {
|
||||||
|
return []entity.DeviceWithDistance{}, nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
uc := NewNearestDeviceUseCase(mockRepo, &mockGeocoder{})
|
||||||
|
|
||||||
|
request := req.NearestDeviceDTO{
|
||||||
|
Longitude: 106.8,
|
||||||
|
Latitude: -6.2,
|
||||||
|
}
|
||||||
|
|
||||||
|
devices, err := uc.GetNearestDevices(request)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("expected no error, got %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(devices) != 0 {
|
||||||
|
t.Errorf("expected 0 devices, got %d", len(devices))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetNearestDevices_ValidationError(t *testing.T) {
|
||||||
|
mockRepo := &mockNearestDeviceRepo{}
|
||||||
|
uc := NewNearestDeviceUseCase(mockRepo, &mockGeocoder{})
|
||||||
|
|
||||||
|
request := req.NearestDeviceDTO{
|
||||||
|
Longitude: 0,
|
||||||
|
Latitude: 0,
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := uc.GetNearestDevices(request)
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("expected validation error for missing required fields")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetNearestDevices_CustomRadiusAndLimit(t *testing.T) {
|
||||||
|
customRadius := 0.3
|
||||||
|
customLimit := 5
|
||||||
|
|
||||||
|
mockRepo := &mockNearestDeviceRepo{
|
||||||
|
GetNearestDevicesFunc: func(longitude, latitude, radius float64, limit int, province, city, district, deviceType *string) ([]entity.DeviceWithDistance, error) {
|
||||||
|
if radius != customRadius {
|
||||||
|
t.Errorf("expected radius %f, got %f", customRadius, radius)
|
||||||
|
}
|
||||||
|
if limit != customLimit {
|
||||||
|
t.Errorf("expected limit %d, got %d", customLimit, limit)
|
||||||
|
}
|
||||||
|
return []entity.DeviceWithDistance{}, nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
uc := NewNearestDeviceUseCase(mockRepo, &mockGeocoder{})
|
||||||
|
|
||||||
|
request := req.NearestDeviceDTO{
|
||||||
|
Longitude: 106.8,
|
||||||
|
Latitude: -6.2,
|
||||||
|
Radius: customRadius,
|
||||||
|
Limit: customLimit,
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := uc.GetNearestDevices(request)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("expected no error, got %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCalculateDistance(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
lat1 float64
|
||||||
|
lng1 float64
|
||||||
|
lat2 float64
|
||||||
|
lng2 float64
|
||||||
|
expected float64
|
||||||
|
delta float64
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "Same point",
|
||||||
|
lat1: -6.2,
|
||||||
|
lng1: 106.8,
|
||||||
|
lat2: -6.2,
|
||||||
|
lng2: 106.8,
|
||||||
|
expected: 0,
|
||||||
|
delta: 0.001,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Jakarta to Bandung",
|
||||||
|
lat1: -6.2088,
|
||||||
|
lng1: 106.8456,
|
||||||
|
lat2: -6.9175,
|
||||||
|
lng2: 107.6191,
|
||||||
|
expected: 115.0,
|
||||||
|
delta: 5.0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Short distance ~350m",
|
||||||
|
lat1: -6.2,
|
||||||
|
lng1: 106.8,
|
||||||
|
lat2: -6.20225,
|
||||||
|
lng2: 106.80225,
|
||||||
|
expected: 0.35,
|
||||||
|
delta: 0.05,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
result := calculateDistance(tt.lat1, tt.lng1, tt.lat2, tt.lng2)
|
||||||
|
if result < tt.expected-tt.delta || result > tt.expected+tt.delta {
|
||||||
|
t.Errorf("calculateDistance(%f, %f, %f, %f) = %f, expected %f ± %f",
|
||||||
|
tt.lat1, tt.lng1, tt.lat2, tt.lng2, result, tt.expected, tt.delta)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetNearestDeviceByID_Success(t *testing.T) {
|
||||||
|
deviceID := uuid.New()
|
||||||
|
province := "Jakarta"
|
||||||
|
city := "Jakarta"
|
||||||
|
|
||||||
|
mockRepo := &mockNearestDeviceRepo{
|
||||||
|
GetDeviceByIDWithConnectionsFunc: func(id uuid.UUID) (entity.Device, error) {
|
||||||
|
if id != deviceID {
|
||||||
|
t.Errorf("expected device ID %s, got %s", deviceID, id)
|
||||||
|
}
|
||||||
|
return entity.Device{
|
||||||
|
ID: deviceID,
|
||||||
|
DeviceCode: "ODP-003",
|
||||||
|
DeviceType: entity.ODP,
|
||||||
|
Longitude: 106.81,
|
||||||
|
Latitude: -6.21,
|
||||||
|
PortAmount: 8,
|
||||||
|
Status: entity.ActiveDev,
|
||||||
|
Province: &province,
|
||||||
|
City: &city,
|
||||||
|
CreatedAt: time.Now(),
|
||||||
|
UpdatedAt: time.Now(),
|
||||||
|
}, nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
uc := NewNearestDeviceUseCase(mockRepo, &mockGeocoder{})
|
||||||
|
|
||||||
|
device, err := uc.GetNearestDeviceByID(deviceID, -6.2, 106.8)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("expected no error, got %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if device.DeviceCode != "ODP-003" {
|
||||||
|
t.Errorf("expected device code ODP-003, got %s", device.DeviceCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
if device.Distance <= 0 {
|
||||||
|
t.Errorf("expected positive distance, got %f", device.Distance)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetNearestDeviceByID_NotFound(t *testing.T) {
|
||||||
|
deviceID := uuid.New()
|
||||||
|
|
||||||
|
mockRepo := &mockNearestDeviceRepo{
|
||||||
|
GetDeviceByIDWithConnectionsFunc: func(id uuid.UUID) (entity.Device, error) {
|
||||||
|
return entity.Device{}, errors.New("record not found")
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
uc := NewNearestDeviceUseCase(mockRepo, &mockGeocoder{})
|
||||||
|
|
||||||
|
_, err := uc.GetNearestDeviceByID(deviceID, -6.2, 106.8)
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("expected error for not found device")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,25 @@
|
||||||
|
OVERVIEW: Utility helpers for image upload, geo conversions, and response construction used across NAM backend.
|
||||||
|
|
||||||
|
WHERE TO LOOK
|
||||||
|
- utils/helper/image_uploader.go
|
||||||
|
- Image uploaders: SaveDeviceImagesBulk, SaveTowerImagesBulk
|
||||||
|
- Image upload constants: MaxFileSize (5MB), TowerUploadDir, DeviceUploadDir
|
||||||
|
- utils/helper/geo_helpers.go
|
||||||
|
- Geo helper functions: ConvertToDeviceResponse, ConvertToOLTResponse, etc.
|
||||||
|
- utils/helper/response_builder.go
|
||||||
|
- Centralized response builders used by delivery layer
|
||||||
|
- Additional helper files under utils/helper/ as needed for conversions and validators
|
||||||
|
|
||||||
|
CONVENTIONS
|
||||||
|
- File naming follows snake_case; exported functions use PascalCase.
|
||||||
|
- Constants use CamelCase with explicit units (MaxFileSize int64 = 5*1024*1024).
|
||||||
|
- Upload directories are defined as constants (TowerUploadDir, DeviceUploadDir) and used across helpers.
|
||||||
|
- Errors propagate with context; use standard errors or fmt.Errorf wrappers.
|
||||||
|
- Tests should cover public helpers; avoid testing private internals directly.
|
||||||
|
|
||||||
|
ANTI-PATTERNS
|
||||||
|
- Do not hardcode absolute system paths; rely on defined constants for directories.
|
||||||
|
- Do not bloat helper files with business logic; keep them pure and focused.
|
||||||
|
- Do not create global mutable state in helpers.
|
||||||
|
- Do not introduce circular imports between helper and delivery layers.
|
||||||
|
- Do not bypass validation for uploads; enforce MaxFileSize and allowed MIME types.
|
||||||
Loading…
Reference in New Issue