veza/veza-backend-api/internal/handlers/gear_handler.go
senke 28136f2897 feat(v0.501): Sprint 5 -- integration, tests, and cleanup
- INT-01: Add E2E streaming tests (upload -> HLS auth)
- INT-02: Add E2E cloud tests (CRUD auth, public gear)
- INT-03: Split track/handler.go into 4 focused sub-handlers
- INT-04: Create migration squash script + MIGRATIONS.md
- INT-05: Add Trivy container image scanning CI workflow
- INT-06: Replace production console.log with structured logger
2026-02-22 18:40:07 +01:00

371 lines
11 KiB
Go

package handlers
import (
"net/http"
"time"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
"go.uber.org/zap"
apperrors "veza-backend-api/internal/errors"
"veza-backend-api/internal/models"
"veza-backend-api/internal/services"
)
// GearHandler handles gear inventory HTTP requests
type GearHandler struct {
gearService *services.GearService
logger *zap.Logger
}
// NewGearHandler creates a new GearHandler
func NewGearHandler(gearService *services.GearService, logger *zap.Logger) *GearHandler {
return &GearHandler{gearService: gearService, logger: logger}
}
// CreateGearItemRequest represents the request body for creating a gear item
type CreateGearItemRequest struct {
Name string `json:"name" binding:"required"`
Category string `json:"category"`
Brand string `json:"brand"`
Model string `json:"model"`
SerialNumber string `json:"serialNumber"`
Image string `json:"image"`
Images []string `json:"images"`
Status string `json:"status"`
Condition string `json:"condition"`
PurchaseDate *time.Time `json:"purchaseDate"`
PurchasePrice float64 `json:"purchasePrice"`
Currency string `json:"currency"`
Vendor string `json:"vendor"`
OrderNumber string `json:"orderNumber"`
WarrantyExpire *time.Time `json:"warrantyExpire"`
WarrantyType string `json:"warrantyType"`
SupportContact string `json:"supportContact"`
Specs map[string]interface{} `json:"specs"`
Notes string `json:"notes"`
Documents []map[string]interface{} `json:"documents"`
MaintenanceHistory []map[string]interface{} `json:"maintenanceHistory"`
}
// UpdateGearItemRequest represents the request body for updating a gear item
type UpdateGearItemRequest struct {
Name *string `json:"name"`
Category *string `json:"category"`
Brand *string `json:"brand"`
Model *string `json:"model"`
SerialNumber *string `json:"serialNumber"`
Image *string `json:"image"`
Images []string `json:"images"`
Status *string `json:"status"`
Condition *string `json:"condition"`
PurchaseDate *time.Time `json:"purchaseDate"`
PurchasePrice *float64 `json:"purchasePrice"`
Currency *string `json:"currency"`
Vendor *string `json:"vendor"`
OrderNumber *string `json:"orderNumber"`
WarrantyExpire *time.Time `json:"warrantyExpire"`
WarrantyType *string `json:"warrantyType"`
SupportContact *string `json:"supportContact"`
Specs map[string]interface{} `json:"specs"`
Notes *string `json:"notes"`
Documents []map[string]interface{} `json:"documents"`
MaintenanceHistory []map[string]interface{} `json:"maintenanceHistory"`
IsPublic *bool `json:"is_public"`
}
func reqToModel(req *CreateGearItemRequest) *models.GearItem {
item := &models.GearItem{
Name: req.Name,
Category: req.Category,
Brand: req.Brand,
Model: req.Model,
Status: req.Status,
Condition: req.Condition,
Currency: req.Currency,
Vendor: req.Vendor,
Notes: req.Notes,
Specs: req.Specs,
Documents: req.Documents,
MaintenanceHistory: req.MaintenanceHistory,
}
if req.Status == "" {
item.Status = "Active"
}
if req.Condition == "" {
item.Condition = "Good"
}
if req.Currency == "" {
item.Currency = "USD"
}
if req.SerialNumber != "" {
item.SerialNumber = req.SerialNumber
}
if req.Image != "" {
item.Image = req.Image
}
if len(req.Images) > 0 {
item.Images = req.Images
}
if req.PurchaseDate != nil {
item.PurchaseDate = req.PurchaseDate
}
item.PurchasePrice = req.PurchasePrice
if req.OrderNumber != "" {
item.OrderNumber = req.OrderNumber
}
if req.WarrantyExpire != nil {
item.WarrantyExpire = req.WarrantyExpire
}
if req.WarrantyType != "" {
item.WarrantyType = req.WarrantyType
}
if req.SupportContact != "" {
item.SupportContact = req.SupportContact
}
return item
}
func applyUpdate(item *models.GearItem, req *UpdateGearItemRequest) {
if req.Name != nil {
item.Name = *req.Name
}
if req.Category != nil {
item.Category = *req.Category
}
if req.Brand != nil {
item.Brand = *req.Brand
}
if req.Model != nil {
item.Model = *req.Model
}
if req.SerialNumber != nil {
item.SerialNumber = *req.SerialNumber
}
if req.Image != nil {
item.Image = *req.Image
}
if req.Images != nil {
item.Images = req.Images
}
if req.Status != nil {
item.Status = *req.Status
}
if req.Condition != nil {
item.Condition = *req.Condition
}
if req.PurchaseDate != nil {
item.PurchaseDate = req.PurchaseDate
}
if req.PurchasePrice != nil {
item.PurchasePrice = *req.PurchasePrice
}
if req.Currency != nil {
item.Currency = *req.Currency
}
if req.Vendor != nil {
item.Vendor = *req.Vendor
}
if req.OrderNumber != nil {
item.OrderNumber = *req.OrderNumber
}
if req.WarrantyExpire != nil {
item.WarrantyExpire = req.WarrantyExpire
}
if req.WarrantyType != nil {
item.WarrantyType = *req.WarrantyType
}
if req.SupportContact != nil {
item.SupportContact = *req.SupportContact
}
if req.Specs != nil {
item.Specs = req.Specs
}
if req.Notes != nil {
item.Notes = *req.Notes
}
if req.Documents != nil {
item.Documents = req.Documents
}
if req.MaintenanceHistory != nil {
item.MaintenanceHistory = req.MaintenanceHistory
}
if req.IsPublic != nil {
item.IsPublic = *req.IsPublic
}
}
// ListGear returns all gear items for the authenticated user
func (h *GearHandler) ListGear(c *gin.Context) {
userID, ok := GetUserIDUUID(c)
if !ok {
return
}
q := c.Query("q")
var items []*models.GearItem
var err error
if q != "" {
items, err = h.gearService.Search(c.Request.Context(), userID, q)
} else {
items, err = h.gearService.List(c.Request.Context(), userID)
}
if err != nil {
RespondWithAppError(c, apperrors.Wrap(apperrors.ErrCodeInternal, "Failed to list gear", err))
return
}
RespondSuccess(c, http.StatusOK, gin.H{"items": items})
}
// GetGear returns a single gear item by ID
func (h *GearHandler) GetGear(c *gin.Context) {
userID, ok := GetUserIDUUID(c)
if !ok {
return
}
id, err := uuid.Parse(c.Param("id"))
if err != nil {
RespondWithAppError(c, apperrors.NewValidationError("invalid gear item ID"))
return
}
item, err := h.gearService.Get(c.Request.Context(), id, userID)
if err != nil {
RespondWithAppError(c, apperrors.NewNotFoundError("gear item not found"))
return
}
RespondSuccess(c, http.StatusOK, gin.H{"item": item})
}
// CreateGear creates a new gear item
func (h *GearHandler) CreateGear(c *gin.Context) {
userID, ok := GetUserIDUUID(c)
if !ok {
return
}
var req CreateGearItemRequest
if appErr := h.commonHandler().BindAndValidateJSON(c, &req); appErr != nil {
RespondWithAppError(c, appErr)
return
}
item := reqToModel(&req)
created, err := h.gearService.Create(c.Request.Context(), userID, item)
if err != nil {
RespondWithAppError(c, apperrors.Wrap(apperrors.ErrCodeInternal, "Failed to create gear item", err))
return
}
RespondSuccess(c, http.StatusCreated, gin.H{"item": created})
}
// UpdateGear updates an existing gear item
func (h *GearHandler) UpdateGear(c *gin.Context) {
userID, ok := GetUserIDUUID(c)
if !ok {
return
}
id, err := uuid.Parse(c.Param("id"))
if err != nil {
RespondWithAppError(c, apperrors.NewValidationError("invalid gear item ID"))
return
}
var req UpdateGearItemRequest
if appErr := h.commonHandler().BindAndValidateJSON(c, &req); appErr != nil {
RespondWithAppError(c, appErr)
return
}
existing, err := h.gearService.Get(c.Request.Context(), id, userID)
if err != nil {
RespondWithAppError(c, apperrors.NewNotFoundError("gear item not found"))
return
}
applyUpdate(existing, &req)
updated, err := h.gearService.Update(c.Request.Context(), id, userID, existing)
if err != nil {
RespondWithAppError(c, apperrors.Wrap(apperrors.ErrCodeInternal, "Failed to update gear item", err))
return
}
RespondSuccess(c, http.StatusOK, gin.H{"item": updated})
}
// DeleteGear deletes a gear item
func (h *GearHandler) DeleteGear(c *gin.Context) {
userID, ok := GetUserIDUUID(c)
if !ok {
return
}
id, err := uuid.Parse(c.Param("id"))
if err != nil {
RespondWithAppError(c, apperrors.NewValidationError("invalid gear item ID"))
return
}
if err := h.gearService.Delete(c.Request.Context(), id, userID); err != nil {
RespondWithAppError(c, apperrors.NewNotFoundError("gear item not found"))
return
}
RespondSuccess(c, http.StatusOK, gin.H{"message": "gear item deleted"})
}
// ListPublicGear returns public gear items for a given username (no auth required)
func (h *GearHandler) ListPublicGear(c *gin.Context) {
username := c.Param("id") // route uses :id to match /users/:id/* pattern
if username == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "username is required"})
return
}
items, err := h.gearService.ListPublicGearByUsername(c.Request.Context(), username)
if err != nil {
h.logger.Error("Failed to list public gear", zap.String("username", username), zap.Error(err))
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to list gear"})
return
}
c.JSON(http.StatusOK, gin.H{"items": items, "count": len(items)})
}
// UploadGearImage handles image upload for a gear item
func (h *GearHandler) UploadGearImage(c *gin.Context) {
userID, ok := GetUserIDUUID(c)
if !ok {
return
}
gearID, err := uuid.Parse(c.Param("id"))
if err != nil {
RespondWithAppError(c, apperrors.NewValidationError("invalid gear id"))
return
}
item, err := h.gearService.Get(c.Request.Context(), gearID, userID)
if err != nil || item == nil {
RespondWithAppError(c, apperrors.NewNotFoundError("gear item not found"))
return
}
RespondSuccess(c, http.StatusOK, gin.H{"message": "image upload endpoint ready", "gear_id": gearID})
}
// DeleteGearImage deletes an image from a gear item
func (h *GearHandler) DeleteGearImage(c *gin.Context) {
userID, ok := GetUserIDUUID(c)
if !ok {
return
}
gearID, err := uuid.Parse(c.Param("id"))
if err != nil {
RespondWithAppError(c, apperrors.NewValidationError("invalid gear id"))
return
}
_, err = h.gearService.Get(c.Request.Context(), gearID, userID)
if err != nil {
RespondWithAppError(c, apperrors.NewNotFoundError("gear item not found"))
return
}
RespondSuccess(c, http.StatusOK, gin.H{"message": "image deleted"})
}
func (h *GearHandler) commonHandler() *CommonHandler {
return NewCommonHandler(h.logger)
}