package handler import ( "encoding/json" "errors" "net/http" "strconv" "time" "github.com/go-chi/chi/v5" "github.com/daniil/homelab-api/internal/middleware" "github.com/daniil/homelab-api/internal/model" "github.com/daniil/homelab-api/internal/repository" "github.com/daniil/homelab-api/internal/service" ) const ownerUserID int64 = 1 type FinanceHandler struct { svc *service.FinanceService } func NewFinanceHandler(svc *service.FinanceService) *FinanceHandler { return &FinanceHandler{svc: svc} } func (h *FinanceHandler) checkOwner(w http.ResponseWriter, r *http.Request) (int64, bool) { userID := middleware.GetUserID(r.Context()) if userID != ownerUserID { writeError(w, "forbidden", http.StatusForbidden) return 0, false } return userID, true } // Categories func (h *FinanceHandler) ListCategories(w http.ResponseWriter, r *http.Request) { userID, ok := h.checkOwner(w, r) if !ok { return } cats, err := h.svc.ListCategories(userID) if err != nil { writeError(w, "failed to fetch categories", http.StatusInternalServerError) return } writeJSON(w, cats, http.StatusOK) } func (h *FinanceHandler) CreateCategory(w http.ResponseWriter, r *http.Request) { userID, ok := h.checkOwner(w, r) if !ok { return } var req model.CreateFinanceCategoryRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { writeError(w, "invalid request body", http.StatusBadRequest) return } if req.Name == "" || req.Type == "" { writeError(w, "name and type are required", http.StatusBadRequest) return } if req.Type != "expense" && req.Type != "income" { writeError(w, "type must be expense or income", http.StatusBadRequest) return } cat, err := h.svc.CreateCategory(userID, &req) if err != nil { writeError(w, "failed to create category", http.StatusInternalServerError) return } writeJSON(w, cat, http.StatusCreated) } func (h *FinanceHandler) UpdateCategory(w http.ResponseWriter, r *http.Request) { userID, ok := h.checkOwner(w, r) if !ok { return } id, err := strconv.ParseInt(chi.URLParam(r, "id"), 10, 64) if err != nil { writeError(w, "invalid id", http.StatusBadRequest) return } var req model.UpdateFinanceCategoryRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { writeError(w, "invalid request body", http.StatusBadRequest) return } cat, err := h.svc.UpdateCategory(id, userID, &req) if err != nil { if errors.Is(err, repository.ErrFinanceCategoryNotFound) { writeError(w, "category not found", http.StatusNotFound) return } writeError(w, "failed to update category", http.StatusInternalServerError) return } writeJSON(w, cat, http.StatusOK) } func (h *FinanceHandler) DeleteCategory(w http.ResponseWriter, r *http.Request) { userID, ok := h.checkOwner(w, r) if !ok { return } id, err := strconv.ParseInt(chi.URLParam(r, "id"), 10, 64) if err != nil { writeError(w, "invalid id", http.StatusBadRequest) return } if err := h.svc.DeleteCategory(id, userID); err != nil { if errors.Is(err, repository.ErrFinanceCategoryNotFound) { writeError(w, "category not found", http.StatusNotFound) return } writeError(w, "failed to delete category", http.StatusInternalServerError) return } w.WriteHeader(http.StatusNoContent) } // Transactions func (h *FinanceHandler) ListTransactions(w http.ResponseWriter, r *http.Request) { userID, ok := h.checkOwner(w, r) if !ok { return } q := r.URL.Query() month, _ := strconv.Atoi(q.Get("month")) year, _ := strconv.Atoi(q.Get("year")) var catID *int64 if c := q.Get("category_id"); c != "" { v, _ := strconv.ParseInt(c, 10, 64) catID = &v } txType := q.Get("type") search := q.Get("search") limit, _ := strconv.Atoi(q.Get("limit")) offset, _ := strconv.Atoi(q.Get("offset")) txs, err := h.svc.ListTransactions(userID, month, year, catID, txType, search, limit, offset) if err != nil { writeError(w, "failed to fetch transactions", http.StatusInternalServerError) return } writeJSON(w, txs, http.StatusOK) } func (h *FinanceHandler) CreateTransaction(w http.ResponseWriter, r *http.Request) { userID, ok := h.checkOwner(w, r) if !ok { return } var req model.CreateFinanceTransactionRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { writeError(w, "invalid request body", http.StatusBadRequest) return } if req.Amount <= 0 { writeError(w, "amount must be positive", http.StatusBadRequest) return } if req.Type != "expense" && req.Type != "income" { writeError(w, "type must be expense or income", http.StatusBadRequest) return } tx, err := h.svc.CreateTransaction(userID, &req) if err != nil { writeError(w, "failed to create transaction", http.StatusInternalServerError) return } writeJSON(w, tx, http.StatusCreated) } func (h *FinanceHandler) UpdateTransaction(w http.ResponseWriter, r *http.Request) { userID, ok := h.checkOwner(w, r) if !ok { return } id, err := strconv.ParseInt(chi.URLParam(r, "id"), 10, 64) if err != nil { writeError(w, "invalid id", http.StatusBadRequest) return } var req model.UpdateFinanceTransactionRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { writeError(w, "invalid request body", http.StatusBadRequest) return } tx, err := h.svc.UpdateTransaction(id, userID, &req) if err != nil { if errors.Is(err, repository.ErrFinanceTransactionNotFound) { writeError(w, "transaction not found", http.StatusNotFound) return } writeError(w, "failed to update transaction", http.StatusInternalServerError) return } writeJSON(w, tx, http.StatusOK) } func (h *FinanceHandler) DeleteTransaction(w http.ResponseWriter, r *http.Request) { userID, ok := h.checkOwner(w, r) if !ok { return } id, err := strconv.ParseInt(chi.URLParam(r, "id"), 10, 64) if err != nil { writeError(w, "invalid id", http.StatusBadRequest) return } if err := h.svc.DeleteTransaction(id, userID); err != nil { if errors.Is(err, repository.ErrFinanceTransactionNotFound) { writeError(w, "transaction not found", http.StatusNotFound) return } writeError(w, "failed to delete transaction", http.StatusInternalServerError) return } w.WriteHeader(http.StatusNoContent) } // Summary & Analytics func (h *FinanceHandler) Summary(w http.ResponseWriter, r *http.Request) { userID, ok := h.checkOwner(w, r) if !ok { return } q := r.URL.Query() month, _ := strconv.Atoi(q.Get("month")) year, _ := strconv.Atoi(q.Get("year")) if month == 0 || year == 0 { now := time.Now() month = int(now.Month()) year = now.Year() } summary, err := h.svc.GetSummary(userID, month, year) if err != nil { writeError(w, "failed to get summary", http.StatusInternalServerError) return } writeJSON(w, summary, http.StatusOK) } func (h *FinanceHandler) Analytics(w http.ResponseWriter, r *http.Request) { userID, ok := h.checkOwner(w, r) if !ok { return } months, _ := strconv.Atoi(r.URL.Query().Get("months")) if months <= 0 { months = 6 } analytics, err := h.svc.GetAnalytics(userID, months) if err != nil { writeError(w, "failed to get analytics", http.StatusInternalServerError) return } writeJSON(w, analytics, http.StatusOK) }