feat(savings): Add savings module with categories, transactions, recurring plans
- Categories: regular, deposits, credits, recurring, multi-user, accounts - Transactions: deposits and withdrawals with user tracking - Recurring plans: monthly payment obligations per user - Stats: overdues calculation with allocation algorithm - Excludes is_account categories from total sums - Documentation: docs/SAVINGS.md
This commit is contained in:
135
internal/handler/habit_freeze.go
Normal file
135
internal/handler/habit_freeze.go
Normal file
@@ -0,0 +1,135 @@
|
||||
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"
|
||||
)
|
||||
|
||||
type HabitFreezeHandler struct {
|
||||
freezeRepo *repository.HabitFreezeRepository
|
||||
habitRepo *repository.HabitRepository
|
||||
}
|
||||
|
||||
func NewHabitFreezeHandler(freezeRepo *repository.HabitFreezeRepository, habitRepo *repository.HabitRepository) *HabitFreezeHandler {
|
||||
return &HabitFreezeHandler{
|
||||
freezeRepo: freezeRepo,
|
||||
habitRepo: habitRepo,
|
||||
}
|
||||
}
|
||||
|
||||
func (h *HabitFreezeHandler) Create(w http.ResponseWriter, r *http.Request) {
|
||||
userID := middleware.GetUserID(r.Context())
|
||||
habitID, err := strconv.ParseInt(chi.URLParam(r, "id"), 10, 64)
|
||||
if err != nil {
|
||||
writeError(w, "invalid habit id", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// Verify habit exists and belongs to user
|
||||
if _, err := h.habitRepo.GetByID(habitID, userID); err != nil {
|
||||
if errors.Is(err, repository.ErrHabitNotFound) {
|
||||
writeError(w, "habit not found", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
writeError(w, "failed to fetch habit", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
var req model.CreateHabitFreezeRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
writeError(w, "invalid request body", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
if req.StartDate == "" || req.EndDate == "" {
|
||||
writeError(w, "start_date and end_date are required", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
startDate, err := time.Parse("2006-01-02", req.StartDate)
|
||||
if err != nil {
|
||||
writeError(w, "invalid start_date format", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
endDate, err := time.Parse("2006-01-02", req.EndDate)
|
||||
if err != nil {
|
||||
writeError(w, "invalid end_date format", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
freeze := &model.HabitFreeze{
|
||||
HabitID: habitID,
|
||||
UserID: userID,
|
||||
StartDate: startDate,
|
||||
EndDate: endDate,
|
||||
Reason: req.Reason,
|
||||
}
|
||||
|
||||
if err := h.freezeRepo.Create(freeze); err != nil {
|
||||
if errors.Is(err, repository.ErrInvalidDateRange) {
|
||||
writeError(w, "end_date must be after start_date", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
writeError(w, "failed to create freeze", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
writeJSON(w, freeze, http.StatusCreated)
|
||||
}
|
||||
|
||||
func (h *HabitFreezeHandler) List(w http.ResponseWriter, r *http.Request) {
|
||||
userID := middleware.GetUserID(r.Context())
|
||||
habitID, err := strconv.ParseInt(chi.URLParam(r, "id"), 10, 64)
|
||||
if err != nil {
|
||||
writeError(w, "invalid habit id", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// Verify habit exists and belongs to user
|
||||
if _, err := h.habitRepo.GetByID(habitID, userID); err != nil {
|
||||
if errors.Is(err, repository.ErrHabitNotFound) {
|
||||
writeError(w, "habit not found", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
writeError(w, "failed to fetch habit", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
freezes, err := h.freezeRepo.GetByHabitID(habitID, userID)
|
||||
if err != nil {
|
||||
writeError(w, "failed to fetch freezes", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
writeJSON(w, freezes, http.StatusOK)
|
||||
}
|
||||
|
||||
func (h *HabitFreezeHandler) Delete(w http.ResponseWriter, r *http.Request) {
|
||||
userID := middleware.GetUserID(r.Context())
|
||||
freezeID, err := strconv.ParseInt(chi.URLParam(r, "freezeId"), 10, 64)
|
||||
if err != nil {
|
||||
writeError(w, "invalid freeze id", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.freezeRepo.Delete(freezeID, userID); err != nil {
|
||||
if errors.Is(err, repository.ErrFreezeNotFound) {
|
||||
writeError(w, "freeze not found", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
writeError(w, "failed to delete freeze", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}
|
||||
@@ -146,6 +146,14 @@ func (h *HabitHandler) Log(w http.ResponseWriter, r *http.Request) {
|
||||
writeError(w, "habit not found", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
if errors.Is(err, service.ErrFutureDate) {
|
||||
writeError(w, "cannot log habit for future date", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
if errors.Is(err, service.ErrAlreadyLogged) {
|
||||
writeError(w, "habit already logged for this date", http.StatusConflict)
|
||||
return
|
||||
}
|
||||
writeError(w, "failed to log habit", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
489
internal/handler/savings.go
Normal file
489
internal/handler/savings.go
Normal file
@@ -0,0 +1,489 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"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"
|
||||
)
|
||||
|
||||
type SavingsHandler struct {
|
||||
repo *repository.SavingsRepository
|
||||
}
|
||||
|
||||
func NewSavingsHandler(repo *repository.SavingsRepository) *SavingsHandler {
|
||||
return &SavingsHandler{repo: repo}
|
||||
}
|
||||
|
||||
// ==================== CATEGORIES ====================
|
||||
|
||||
func (h *SavingsHandler) ListCategories(w http.ResponseWriter, r *http.Request) {
|
||||
userID := middleware.GetUserID(r.Context())
|
||||
|
||||
categories, err := h.repo.ListCategories(userID)
|
||||
if err != nil {
|
||||
writeError(w, "failed to fetch categories", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// Process and calculate balances
|
||||
for i := range categories {
|
||||
categories[i].ProcessForJSON()
|
||||
balance, _ := h.repo.GetCategoryBalance(categories[i].ID)
|
||||
categories[i].CurrentAmount = balance
|
||||
|
||||
if categories[i].IsRecurring {
|
||||
total, _ := h.repo.GetRecurringTotalAmount(categories[i].ID)
|
||||
categories[i].RecurringTotalAmount = total
|
||||
}
|
||||
}
|
||||
|
||||
writeJSON(w, categories, http.StatusOK)
|
||||
}
|
||||
|
||||
func (h *SavingsHandler) GetCategory(w http.ResponseWriter, r *http.Request) {
|
||||
userID := middleware.GetUserID(r.Context())
|
||||
categoryID, err := strconv.ParseInt(chi.URLParam(r, "id"), 10, 64)
|
||||
if err != nil {
|
||||
writeError(w, "invalid category id", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
category, err := h.repo.GetCategory(categoryID, userID)
|
||||
if err != nil {
|
||||
if errors.Is(err, repository.ErrCategoryNotFound) {
|
||||
writeError(w, "category not found", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
writeError(w, "failed to fetch category", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
category.ProcessForJSON()
|
||||
balance, _ := h.repo.GetCategoryBalance(category.ID)
|
||||
category.CurrentAmount = balance
|
||||
|
||||
if category.IsRecurring {
|
||||
total, _ := h.repo.GetRecurringTotalAmount(category.ID)
|
||||
category.RecurringTotalAmount = total
|
||||
}
|
||||
|
||||
writeJSON(w, category, http.StatusOK)
|
||||
}
|
||||
|
||||
func (h *SavingsHandler) CreateCategory(w http.ResponseWriter, r *http.Request) {
|
||||
userID := middleware.GetUserID(r.Context())
|
||||
|
||||
var req model.CreateSavingsCategoryRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
writeError(w, "invalid request body", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
if req.Name == "" {
|
||||
writeError(w, "name is required", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
category, err := h.repo.CreateCategory(userID, &req)
|
||||
if err != nil {
|
||||
writeError(w, "failed to create category", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
category.ProcessForJSON()
|
||||
writeJSON(w, category, http.StatusCreated)
|
||||
}
|
||||
|
||||
func (h *SavingsHandler) UpdateCategory(w http.ResponseWriter, r *http.Request) {
|
||||
userID := middleware.GetUserID(r.Context())
|
||||
categoryID, err := strconv.ParseInt(chi.URLParam(r, "id"), 10, 64)
|
||||
if err != nil {
|
||||
writeError(w, "invalid category id", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
var req model.UpdateSavingsCategoryRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
writeError(w, "invalid request body", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
category, err := h.repo.UpdateCategory(categoryID, userID, &req)
|
||||
if err != nil {
|
||||
if errors.Is(err, repository.ErrCategoryNotFound) {
|
||||
writeError(w, "category not found", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
if errors.Is(err, repository.ErrNotAuthorized) {
|
||||
writeError(w, "not authorized", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
writeError(w, "failed to update category", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
category.ProcessForJSON()
|
||||
balance, _ := h.repo.GetCategoryBalance(category.ID)
|
||||
category.CurrentAmount = balance
|
||||
|
||||
writeJSON(w, category, http.StatusOK)
|
||||
}
|
||||
|
||||
func (h *SavingsHandler) DeleteCategory(w http.ResponseWriter, r *http.Request) {
|
||||
userID := middleware.GetUserID(r.Context())
|
||||
categoryID, err := strconv.ParseInt(chi.URLParam(r, "id"), 10, 64)
|
||||
if err != nil {
|
||||
writeError(w, "invalid category id", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.repo.DeleteCategory(categoryID, userID); err != nil {
|
||||
if errors.Is(err, repository.ErrCategoryNotFound) {
|
||||
writeError(w, "category not found", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
writeError(w, "failed to delete category", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}
|
||||
|
||||
// ==================== TRANSACTIONS ====================
|
||||
|
||||
func (h *SavingsHandler) ListTransactions(w http.ResponseWriter, r *http.Request) {
|
||||
userID := middleware.GetUserID(r.Context())
|
||||
|
||||
var categoryID *int64
|
||||
if catIDStr := r.URL.Query().Get("category_id"); catIDStr != "" {
|
||||
id, err := strconv.ParseInt(catIDStr, 10, 64)
|
||||
if err == nil {
|
||||
categoryID = &id
|
||||
}
|
||||
}
|
||||
|
||||
limit := 100
|
||||
if l := r.URL.Query().Get("limit"); l != "" {
|
||||
if parsed, err := strconv.Atoi(l); err == nil && parsed > 0 {
|
||||
limit = parsed
|
||||
}
|
||||
}
|
||||
|
||||
offset := 0
|
||||
if o := r.URL.Query().Get("offset"); o != "" {
|
||||
if parsed, err := strconv.Atoi(o); err == nil && parsed >= 0 {
|
||||
offset = parsed
|
||||
}
|
||||
}
|
||||
|
||||
transactions, err := h.repo.ListTransactions(userID, categoryID, limit, offset)
|
||||
if err != nil {
|
||||
writeError(w, "failed to fetch transactions", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
writeJSON(w, transactions, http.StatusOK)
|
||||
}
|
||||
|
||||
func (h *SavingsHandler) GetTransaction(w http.ResponseWriter, r *http.Request) {
|
||||
userID := middleware.GetUserID(r.Context())
|
||||
txID, err := strconv.ParseInt(chi.URLParam(r, "id"), 10, 64)
|
||||
if err != nil {
|
||||
writeError(w, "invalid transaction id", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
transaction, err := h.repo.GetTransaction(txID, userID)
|
||||
if err != nil {
|
||||
if errors.Is(err, repository.ErrTransactionNotFound) {
|
||||
writeError(w, "transaction not found", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
writeError(w, "failed to fetch transaction", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
writeJSON(w, transaction, http.StatusOK)
|
||||
}
|
||||
|
||||
func (h *SavingsHandler) CreateTransaction(w http.ResponseWriter, r *http.Request) {
|
||||
userID := middleware.GetUserID(r.Context())
|
||||
|
||||
var req model.CreateSavingsTransactionRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
writeError(w, "invalid request body", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
if req.CategoryID == 0 {
|
||||
writeError(w, "category_id is required", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
if req.Amount <= 0 {
|
||||
writeError(w, "amount must be positive", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
if req.Type != "deposit" && req.Type != "withdrawal" {
|
||||
writeError(w, "type must be 'deposit' or 'withdrawal'", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
if req.Date == "" {
|
||||
writeError(w, "date is required", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
transaction, err := h.repo.CreateTransaction(userID, &req)
|
||||
if err != nil {
|
||||
if errors.Is(err, repository.ErrCategoryNotFound) {
|
||||
writeError(w, "category not found", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
writeError(w, "failed to create transaction: "+err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
writeJSON(w, transaction, http.StatusCreated)
|
||||
}
|
||||
|
||||
func (h *SavingsHandler) UpdateTransaction(w http.ResponseWriter, r *http.Request) {
|
||||
userID := middleware.GetUserID(r.Context())
|
||||
txID, err := strconv.ParseInt(chi.URLParam(r, "id"), 10, 64)
|
||||
if err != nil {
|
||||
writeError(w, "invalid transaction id", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
var req model.UpdateSavingsTransactionRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
writeError(w, "invalid request body", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
transaction, err := h.repo.UpdateTransaction(txID, userID, &req)
|
||||
if err != nil {
|
||||
if errors.Is(err, repository.ErrTransactionNotFound) {
|
||||
writeError(w, "transaction not found", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
if errors.Is(err, repository.ErrNotAuthorized) {
|
||||
writeError(w, "not authorized", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
writeError(w, "failed to update transaction", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
writeJSON(w, transaction, http.StatusOK)
|
||||
}
|
||||
|
||||
func (h *SavingsHandler) DeleteTransaction(w http.ResponseWriter, r *http.Request) {
|
||||
userID := middleware.GetUserID(r.Context())
|
||||
txID, err := strconv.ParseInt(chi.URLParam(r, "id"), 10, 64)
|
||||
if err != nil {
|
||||
writeError(w, "invalid transaction id", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.repo.DeleteTransaction(txID, userID); err != nil {
|
||||
if errors.Is(err, repository.ErrTransactionNotFound) {
|
||||
writeError(w, "transaction not found", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
writeError(w, "failed to delete transaction", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}
|
||||
|
||||
// ==================== STATS ====================
|
||||
|
||||
func (h *SavingsHandler) Stats(w http.ResponseWriter, r *http.Request) {
|
||||
userID := middleware.GetUserID(r.Context())
|
||||
|
||||
stats, err := h.repo.GetStats(userID)
|
||||
if err != nil {
|
||||
writeError(w, "failed to fetch stats", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
writeJSON(w, stats, http.StatusOK)
|
||||
}
|
||||
|
||||
// ==================== MEMBERS ====================
|
||||
|
||||
func (h *SavingsHandler) ListMembers(w http.ResponseWriter, r *http.Request) {
|
||||
categoryID, err := strconv.ParseInt(chi.URLParam(r, "id"), 10, 64)
|
||||
if err != nil {
|
||||
writeError(w, "invalid category id", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
members, err := h.repo.GetCategoryMembers(categoryID)
|
||||
if err != nil {
|
||||
writeError(w, "failed to fetch members", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
writeJSON(w, members, http.StatusOK)
|
||||
}
|
||||
|
||||
func (h *SavingsHandler) AddMember(w http.ResponseWriter, r *http.Request) {
|
||||
userID := middleware.GetUserID(r.Context())
|
||||
categoryID, err := strconv.ParseInt(chi.URLParam(r, "id"), 10, 64)
|
||||
if err != nil {
|
||||
writeError(w, "invalid category id", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// Check ownership
|
||||
category, err := h.repo.GetCategory(categoryID, userID)
|
||||
if err != nil {
|
||||
writeError(w, "category not found", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
if category.UserID != userID {
|
||||
writeError(w, "not authorized", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
|
||||
var req struct {
|
||||
UserID int64 `json:"user_id"`
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
writeError(w, "invalid request body", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.repo.AddCategoryMember(categoryID, req.UserID); err != nil {
|
||||
writeError(w, "failed to add member", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
members, _ := h.repo.GetCategoryMembers(categoryID)
|
||||
writeJSON(w, members, http.StatusOK)
|
||||
}
|
||||
|
||||
func (h *SavingsHandler) RemoveMember(w http.ResponseWriter, r *http.Request) {
|
||||
userID := middleware.GetUserID(r.Context())
|
||||
categoryID, err := strconv.ParseInt(chi.URLParam(r, "id"), 10, 64)
|
||||
if err != nil {
|
||||
writeError(w, "invalid category id", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
memberUserID, err := strconv.ParseInt(chi.URLParam(r, "userId"), 10, 64)
|
||||
if err != nil {
|
||||
writeError(w, "invalid user id", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// Check ownership
|
||||
category, err := h.repo.GetCategory(categoryID, userID)
|
||||
if err != nil {
|
||||
writeError(w, "category not found", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
if category.UserID != userID {
|
||||
writeError(w, "not authorized", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.repo.RemoveCategoryMember(categoryID, memberUserID); err != nil {
|
||||
writeError(w, "failed to remove member", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}
|
||||
|
||||
// ==================== RECURRING PLANS ====================
|
||||
|
||||
func (h *SavingsHandler) ListRecurringPlans(w http.ResponseWriter, r *http.Request) {
|
||||
categoryID, err := strconv.ParseInt(chi.URLParam(r, "id"), 10, 64)
|
||||
if err != nil {
|
||||
writeError(w, "invalid category id", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
plans, err := h.repo.ListRecurringPlans(categoryID)
|
||||
if err != nil {
|
||||
writeError(w, "failed to fetch recurring plans", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
writeJSON(w, plans, http.StatusOK)
|
||||
}
|
||||
|
||||
func (h *SavingsHandler) CreateRecurringPlan(w http.ResponseWriter, r *http.Request) {
|
||||
userID := middleware.GetUserID(r.Context())
|
||||
categoryID, err := strconv.ParseInt(chi.URLParam(r, "id"), 10, 64)
|
||||
if err != nil {
|
||||
writeError(w, "invalid category id", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// Check access
|
||||
_, err = h.repo.GetCategory(categoryID, userID)
|
||||
if err != nil {
|
||||
writeError(w, "category not found", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
var req model.CreateRecurringPlanRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
writeError(w, "invalid request body", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
plan, err := h.repo.CreateRecurringPlan(categoryID, &req)
|
||||
if err != nil {
|
||||
writeError(w, "failed to create recurring plan: "+err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
writeJSON(w, plan, http.StatusCreated)
|
||||
}
|
||||
|
||||
func (h *SavingsHandler) DeleteRecurringPlan(w http.ResponseWriter, r *http.Request) {
|
||||
planID, err := strconv.ParseInt(chi.URLParam(r, "planId"), 10, 64)
|
||||
if err != nil {
|
||||
writeError(w, "invalid plan id", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.repo.DeleteRecurringPlan(planID); err != nil {
|
||||
writeError(w, "failed to delete recurring plan", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}
|
||||
|
||||
func (h *SavingsHandler) UpdateRecurringPlan(w http.ResponseWriter, r *http.Request) {
|
||||
planID, err := strconv.ParseInt(chi.URLParam(r, "planId"), 10, 64)
|
||||
if err != nil {
|
||||
writeError(w, "invalid plan id", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
var req model.UpdateRecurringPlanRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
writeError(w, "invalid request body", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
plan, err := h.repo.UpdateRecurringPlan(planID, &req)
|
||||
if err != nil {
|
||||
writeError(w, "failed to update recurring plan: "+err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
writeJSON(w, plan, http.StatusOK)
|
||||
}
|
||||
Reference in New Issue
Block a user