package userproduct import ( "context" "encoding/json" "errors" "log/slog" "net/http" "github.com/food-ai/backend/internal/infra/middleware" "github.com/go-chi/chi/v5" ) // UserProductRepository is the data layer interface used by Handler. type UserProductRepository interface { List(ctx context.Context, userID string) ([]*UserProduct, error) Create(ctx context.Context, userID string, req CreateRequest) (*UserProduct, error) BatchCreate(ctx context.Context, userID string, items []CreateRequest) ([]*UserProduct, error) Update(ctx context.Context, id, userID string, req UpdateRequest) (*UserProduct, error) Delete(ctx context.Context, id, userID string) error } // Handler handles /user-products HTTP requests. type Handler struct { repo UserProductRepository } // NewHandler creates a new Handler. func NewHandler(repo UserProductRepository) *Handler { return &Handler{repo: repo} } // List handles GET /user-products. func (handler *Handler) List(responseWriter http.ResponseWriter, request *http.Request) { userID := middleware.UserIDFromCtx(request.Context()) userProducts, listError := handler.repo.List(request.Context(), userID) if listError != nil { slog.ErrorContext(request.Context(), "list user products", "user_id", userID, "err", listError) writeErrorJSON(responseWriter, request, http.StatusInternalServerError, "failed to list user products") return } if userProducts == nil { userProducts = []*UserProduct{} } writeJSON(responseWriter, http.StatusOK, userProducts) } // Create handles POST /user-products. func (handler *Handler) Create(responseWriter http.ResponseWriter, request *http.Request) { userID := middleware.UserIDFromCtx(request.Context()) var req CreateRequest if decodeError := json.NewDecoder(request.Body).Decode(&req); decodeError != nil { writeErrorJSON(responseWriter, request, http.StatusBadRequest, "invalid request body") return } if req.Name == "" { writeErrorJSON(responseWriter, request, http.StatusBadRequest, "name is required") return } userProduct, createError := handler.repo.Create(request.Context(), userID, req) if createError != nil { slog.ErrorContext(request.Context(), "create user product", "user_id", userID, "err", createError) writeErrorJSON(responseWriter, request, http.StatusInternalServerError, "failed to create user product") return } writeJSON(responseWriter, http.StatusCreated, userProduct) } // BatchCreate handles POST /user-products/batch. func (handler *Handler) BatchCreate(responseWriter http.ResponseWriter, request *http.Request) { userID := middleware.UserIDFromCtx(request.Context()) var items []CreateRequest if decodeError := json.NewDecoder(request.Body).Decode(&items); decodeError != nil { writeErrorJSON(responseWriter, request, http.StatusBadRequest, "invalid request body") return } if len(items) == 0 { writeJSON(responseWriter, http.StatusCreated, []*UserProduct{}) return } userProducts, batchError := handler.repo.BatchCreate(request.Context(), userID, items) if batchError != nil { slog.ErrorContext(request.Context(), "batch create user products", "user_id", userID, "err", batchError) writeErrorJSON(responseWriter, request, http.StatusInternalServerError, "failed to create user products") return } writeJSON(responseWriter, http.StatusCreated, userProducts) } // Update handles PUT /user-products/{id}. func (handler *Handler) Update(responseWriter http.ResponseWriter, request *http.Request) { userID := middleware.UserIDFromCtx(request.Context()) id := chi.URLParam(request, "id") var req UpdateRequest if decodeError := json.NewDecoder(request.Body).Decode(&req); decodeError != nil { writeErrorJSON(responseWriter, request, http.StatusBadRequest, "invalid request body") return } userProduct, updateError := handler.repo.Update(request.Context(), id, userID, req) if errors.Is(updateError, ErrNotFound) { writeErrorJSON(responseWriter, request, http.StatusNotFound, "user product not found") return } if updateError != nil { slog.ErrorContext(request.Context(), "update user product", "id", id, "err", updateError) writeErrorJSON(responseWriter, request, http.StatusInternalServerError, "failed to update user product") return } writeJSON(responseWriter, http.StatusOK, userProduct) } // Delete handles DELETE /user-products/{id}. func (handler *Handler) Delete(responseWriter http.ResponseWriter, request *http.Request) { userID := middleware.UserIDFromCtx(request.Context()) id := chi.URLParam(request, "id") if deleteError := handler.repo.Delete(request.Context(), id, userID); deleteError != nil { if errors.Is(deleteError, ErrNotFound) { writeErrorJSON(responseWriter, request, http.StatusNotFound, "user product not found") return } slog.ErrorContext(request.Context(), "delete user product", "id", id, "err", deleteError) writeErrorJSON(responseWriter, request, http.StatusInternalServerError, "failed to delete user product") return } responseWriter.WriteHeader(http.StatusNoContent) } type errorResponse struct { Error string `json:"error"` RequestID string `json:"request_id,omitempty"` } func writeErrorJSON(responseWriter http.ResponseWriter, request *http.Request, status int, msg string) { responseWriter.Header().Set("Content-Type", "application/json") responseWriter.WriteHeader(status) _ = json.NewEncoder(responseWriter).Encode(errorResponse{ Error: msg, RequestID: middleware.RequestIDFromCtx(request.Context()), }) } func writeJSON(responseWriter http.ResponseWriter, status int, value any) { responseWriter.Header().Set("Content-Type", "application/json") responseWriter.WriteHeader(status) _ = json.NewEncoder(responseWriter).Encode(value) }