277 lines
7.2 KiB
Go
277 lines
7.2 KiB
Go
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
|
|
}
|
|
q := r.URL.Query()
|
|
months, _ := strconv.Atoi(q.Get("months"))
|
|
if months <= 0 {
|
|
months = 6
|
|
}
|
|
now := time.Now()
|
|
month, _ := strconv.Atoi(q.Get("month"))
|
|
year, _ := strconv.Atoi(q.Get("year"))
|
|
if month <= 0 {
|
|
month = int(now.Month())
|
|
}
|
|
if year <= 0 {
|
|
year = now.Year()
|
|
}
|
|
analytics, err := h.svc.GetAnalytics(userID, months, month, year)
|
|
if err != nil {
|
|
writeError(w, "failed to get analytics", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
writeJSON(w, analytics, http.StatusOK)
|
|
}
|