Refactored transaction input handling and removed unused receipt-related definitions in Swagger.

This commit is contained in:
2026-05-09 12:53:36 +03:00
parent a57f918d23
commit c3f90b57c2
16 changed files with 410 additions and 838 deletions
+4 -9
View File
@@ -2,8 +2,8 @@ package routers
import (
"FamilyHub/src/api/dto"
"FamilyHub/src/api/requests"
"FamilyHub/src/api/services"
"FamilyHub/src/domain"
"net/http"
"github.com/gin-gonic/gin"
@@ -45,15 +45,10 @@ func (router *ActivitiesRouter) List(c *gin.Context) {
return
}
activities, filter, err := router.service.List(c.Request.Context(), domain.ActivityLogListFilter{
FamilyID: query.FamilyID,
UserID: query.UserID,
Limit: query.Limit,
Offset: query.Offset,
})
filter := requests.BuildActivityListFilter(query)
activities, filter, err := router.service.List(c.Request.Context(), filter)
if err != nil {
logInternalError(c, "activity request", err)
c.JSON(http.StatusInternalServerError, dto.ErrorResponse{Message: "internal server error"})
handleActivityError(c, err)
return
}
+36
View File
@@ -2,7 +2,10 @@ package routers
import (
"FamilyHub/src/api/dto"
"FamilyHub/src/api/requests"
"FamilyHub/src/api/services"
receiptServiceIntegration "FamilyHub/src/integrations/receiptProvider"
"database/sql"
"errors"
"log"
"net/http"
@@ -60,3 +63,36 @@ func handleReceiptError(c *gin.Context, err error) {
c.JSON(http.StatusInternalServerError, dto.ErrorResponse{Message: "internal server error"})
}
}
func handleActivityError(c *gin.Context, err error) {
logInternalError(c, "activity request", err)
c.JSON(http.StatusInternalServerError, dto.ErrorResponse{Message: "internal server error"})
}
func handleFamilyError(c *gin.Context, err error) {
switch {
case errors.Is(err, services.ErrFamilyNotFound):
c.JSON(http.StatusNotFound, dto.ErrorResponse{Message: err.Error()})
case errors.Is(err, sql.ErrNoRows):
c.JSON(http.StatusNotFound, dto.ErrorResponse{Message: "family not found"})
case errors.Is(err, requests.ErrFamilyNameRequired):
c.JSON(http.StatusBadRequest, dto.ErrorResponse{Message: err.Error()})
default:
logInternalError(c, "family request", err)
c.JSON(http.StatusInternalServerError, dto.ErrorResponse{Message: "internal server error"})
}
}
func handleUserError(c *gin.Context, err error) {
switch {
case errors.Is(err, services.ErrUserNotFound):
c.JSON(http.StatusNotFound, dto.ErrorResponse{Message: err.Error()})
case errors.Is(err, services.ErrInvalidPatch):
c.JSON(http.StatusBadRequest, dto.ErrorResponse{Message: err.Error()})
case errors.Is(err, services.ErrTelegramIDMissing):
c.JSON(http.StatusBadRequest, dto.ErrorResponse{Message: err.Error()})
default:
logInternalError(c, "user request", err)
c.JSON(http.StatusInternalServerError, dto.ErrorResponse{Message: "internal server error"})
}
}
+24 -37
View File
@@ -1,12 +1,11 @@
package routers
import (
"FamilyHub/src/api/dto"
"FamilyHub/src/api/requests"
"FamilyHub/src/api/services"
"FamilyHub/src/domain"
"database/sql"
"errors"
"net/http"
"strconv"
"github.com/gin-gonic/gin"
)
@@ -37,15 +36,15 @@ func (router *FamiliesRouter) RegisterRoutes(r *gin.RouterGroup) {
// @Produce json
// @Param family body domain.CreateFamilyRequest true "Family info"
// @Success 201 {object} domain.FamilyResponse
// @Failure 400 {object} map[string]string "invalid body"
// @Failure 500 {object} map[string]string "internal server error"
// @Failure 400 {object} dto.ErrorResponse "invalid body"
// @Failure 500 {object} dto.ErrorResponse "internal server error"
// @Router /api/v1/families [post]
func (router *FamiliesRouter) Create(c *gin.Context) {
var req domain.CreateFamilyRequest
var resp domain.FamilyResponse
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
c.JSON(http.StatusBadRequest, dto.ErrorResponse{Message: err.Error()})
return
}
@@ -66,16 +65,16 @@ func (router *FamiliesRouter) Create(c *gin.Context) {
// @Produce json
// @Param id path int true "Family ID"
// @Success 200 {object} domain.FamilyResponse
// @Failure 400 {object} map[string]string "invalid id"
// @Failure 404 {object} map[string]string "family not found"
// @Failure 500 {object} map[string]string "internal server error"
// @Failure 400 {object} dto.ErrorResponse "invalid id"
// @Failure 404 {object} dto.ErrorResponse "family not found"
// @Failure 500 {object} dto.ErrorResponse "internal server error"
// @Router /api/v1/families/{id} [get]
func (router *FamiliesRouter) Read(c *gin.Context) {
var resp domain.FamilyResponse
id, err := strconv.ParseInt(c.Param("id"), 10, 64)
id, err := requests.ParseInt64(c.Param("id"), "invalid id")
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid id"})
c.JSON(http.StatusBadRequest, dto.ErrorResponse{Message: err.Error()})
return
}
@@ -97,27 +96,27 @@ func (router *FamiliesRouter) Read(c *gin.Context) {
// @Param id path int true "Family ID"
// @Param family body domain.UpdateFamilyRequest true "Данные для обновления"
// @Success 200 {object} domain.FamilyResponse
// @Failure 400 {object} map[string]string "invalid id or invalid body"
// @Failure 400 {object} map[string]string "name is required"
// @Failure 404 {object} map[string]string "family not found"
// @Failure 500 {object} map[string]string "internal server error"
// @Failure 400 {object} dto.ErrorResponse "invalid id or invalid body"
// @Failure 400 {object} dto.ErrorResponse "name is required"
// @Failure 404 {object} dto.ErrorResponse "family not found"
// @Failure 500 {object} dto.ErrorResponse "internal server error"
// @Router /api/v1/families/{id} [patch]
func (router *FamiliesRouter) Update(c *gin.Context) {
var resp domain.FamilyResponse
id, err := strconv.ParseInt(c.Param("id"), 10, 64)
id, err := requests.ParseInt64(c.Param("id"), "invalid id")
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid id"})
c.JSON(http.StatusBadRequest, dto.ErrorResponse{Message: err.Error()})
return
}
var req domain.UpdateFamilyRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
c.JSON(http.StatusBadRequest, dto.ErrorResponse{Message: err.Error()})
return
}
if req.Name == nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "name is required"})
if err := requests.ValidateFamilyUpdate(req); err != nil {
c.JSON(http.StatusBadRequest, dto.ErrorResponse{Message: err.Error()})
return
}
@@ -138,14 +137,14 @@ func (router *FamiliesRouter) Update(c *gin.Context) {
// @Produce json
// @Param id path int true "Family ID"
// @Success 204 {string} string "no content"
// @Failure 400 {object} map[string]string "invalid id"
// @Failure 404 {object} map[string]string "family not found"
// @Failure 500 {object} map[string]string "internal server error"
// @Failure 400 {object} dto.ErrorResponse "invalid id"
// @Failure 404 {object} dto.ErrorResponse "family not found"
// @Failure 500 {object} dto.ErrorResponse "internal server error"
// @Router /api/v1/families/{id} [delete]
func (router *FamiliesRouter) Delete(c *gin.Context) {
id, err := strconv.ParseInt(c.Param("id"), 10, 64)
id, err := requests.ParseInt64(c.Param("id"), "invalid id")
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid id"})
c.JSON(http.StatusBadRequest, dto.ErrorResponse{Message: err.Error()})
return
}
@@ -156,15 +155,3 @@ func (router *FamiliesRouter) Delete(c *gin.Context) {
c.Status(http.StatusNoContent)
}
func handleFamilyError(c *gin.Context, err error) {
switch {
case errors.Is(err, services.ErrFamilyNotFound):
c.JSON(http.StatusNotFound, gin.H{"error": err.Error()})
case errors.Is(err, sql.ErrNoRows):
c.JSON(http.StatusNotFound, gin.H{"error": "family not found"})
default:
logInternalError(c, "family request", err)
c.JSON(http.StatusInternalServerError, gin.H{"error": "internal server error"})
}
}
+2 -2
View File
@@ -93,7 +93,7 @@ func TestFamiliesRouter_Create(t *testing.T) {
r.ServeHTTP(w, req)
require.Equal(t, http.StatusBadRequest, w.Code)
assert.Contains(t, w.Body.String(), "error")
assert.Contains(t, w.Body.String(), "message")
})
t.Run("internal error", func(t *testing.T) {
@@ -205,7 +205,7 @@ func TestFamiliesRouter_Update(t *testing.T) {
r.ServeHTTP(w, req)
require.Equal(t, http.StatusBadRequest, w.Code)
assert.Contains(t, w.Body.String(), "error")
assert.Contains(t, w.Body.String(), "message")
})
t.Run("bad request on missing name", func(t *testing.T) {
+48 -3
View File
@@ -41,12 +41,16 @@ func (router *TransactionsRouter) RegisterRoutes(r *gin.RouterGroup) {
// Create GoDoc
// @Summary Создать транзакцию
// @Description Создает новую транзакцию и при необходимости привязывает к ней чек
// @Description Создает транзакцию одним из трех способов.
// @Description 1. application/json: ручная транзакция с полями family_id, created_by, type, category, amount, datetime.
// @Description 2. application/json: транзакция по чеку с полями family_id, created_by, receipt_number, receipt_date.
// @Description 3. multipart/form-data: транзакция по фото чека с полями photo, family_id, created_by и опциональными type, category, description.
// @Description В одном JSON-запросе нельзя смешивать ручные поля транзакции с полями receipt_number и receipt_date.
// @Tags Transactions
// @Accept json
// @Accept multipart/form-data
// @Produce json
// @Param transaction body dto.CreateTransactionRequest true "Transaction payload"
// @Param transaction body dto.CreateTransactionRequest false "JSON payload for manual or receipt-based transaction creation"
// @Success 201 {object} dto.TransactionResponse
// @Failure 400 {object} dto.ErrorResponse
// @Failure 404 {object} dto.ErrorResponse
@@ -101,7 +105,25 @@ func (router *TransactionsRouter) createFromMultipart(c *gin.Context) {
return
}
input, err := requests.BuildPhotoCreateTransactionInput(c, imageBytes)
familyID, err := parseOptionalInt64Form(c, "family_id")
if err != nil {
c.JSON(http.StatusBadRequest, dto.ErrorResponse{Message: err.Error()})
return
}
createdBy, err := parseOptionalInt64Form(c, "created_by")
if err != nil {
c.JSON(http.StatusBadRequest, dto.ErrorResponse{Message: err.Error()})
return
}
input, err := requests.BuildPhotoCreateTransactionInput(requests.PhotoCreateTransactionFields{
Image: imageBytes,
FamilyID: familyID,
CreatedBy: createdBy,
Type: parseOptionalStringForm(c, "type"),
Category: parseOptionalStringForm(c, "category"),
Description: parseOptionalStringForm(c, "description"),
})
if err != nil {
c.JSON(http.StatusBadRequest, dto.ErrorResponse{Message: err.Error()})
return
@@ -364,3 +386,26 @@ func transactionQueryToFilter(query dto.ListTransactionsQuery) (domain.Transacti
return filter, nil
}
func parseOptionalInt64Form(c *gin.Context, key string) (*int64, error) {
value := strings.TrimSpace(c.PostForm(key))
if value == "" {
return nil, nil
}
parsed, err := strconv.ParseInt(value, 10, 64)
if err != nil {
return nil, errors.New(key + " must be int64")
}
return &parsed, nil
}
func parseOptionalStringForm(c *gin.Context, key string) *string {
value := strings.TrimSpace(c.PostForm(key))
if value == "" {
return nil
}
return &value
}
+31 -45
View File
@@ -1,11 +1,11 @@
package routers
import (
"FamilyHub/src/api/dto"
"FamilyHub/src/api/requests"
"FamilyHub/src/api/services"
"FamilyHub/src/domain"
"errors"
"net/http"
"strconv"
"github.com/gin-gonic/gin"
)
@@ -36,21 +36,21 @@ func (router *UsersRouter) RegisterRoutes(r *gin.RouterGroup) {
// @Produce json
// @Param user body domain.CreateUserRequest true "User info"
// @Success 201 {object} domain.UserResponse
// @Failure 400 {object} domain.UserErrorResponse
// @Failure 500 {object} domain.UserErrorResponse
// @Failure 400 {object} dto.ErrorResponse
// @Failure 500 {object} dto.ErrorResponse
// @Router /api/v1/users [post]
func (router *UsersRouter) Create(c *gin.Context) {
var req domain.CreateUserRequest
var resp domain.UserResponse
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, domain.UserErrorResponse{Error: err.Error()})
c.JSON(http.StatusBadRequest, dto.ErrorResponse{Message: err.Error()})
return
}
user, err := router.service.Create(c.Request.Context(), req)
if err != nil {
handleError(c, err)
handleUserError(c, err)
return
}
@@ -65,21 +65,21 @@ func (router *UsersRouter) Create(c *gin.Context) {
// @Produce json
// @Param id path int true "User ID"
// @Success 200 {object} domain.UserResponse
// @Failure 400 {object} domain.UserErrorResponse "invalid id"
// @Failure 404 {object} domain.UserErrorResponse "user not found"
// @Failure 500 {object} domain.UserErrorResponse "internal server error"
// @Failure 400 {object} dto.ErrorResponse "invalid id"
// @Failure 404 {object} dto.ErrorResponse "user not found"
// @Failure 500 {object} dto.ErrorResponse "internal server error"
// @Router /api/v1/users/{id} [get]
func (router *UsersRouter) Read(c *gin.Context) {
var resp domain.UserResponse
id, err := strconv.ParseInt(c.Param("id"), 10, 64)
id, err := requests.ParseInt64(c.Param("id"), "invalid id")
if err != nil {
c.JSON(http.StatusBadRequest, domain.UserErrorResponse{Error: "invalid id"})
c.JSON(http.StatusBadRequest, dto.ErrorResponse{Message: err.Error()})
return
}
user, err := router.service.GetByID(c.Request.Context(), id)
if err != nil {
handleError(c, err)
handleUserError(c, err)
return
}
@@ -94,21 +94,21 @@ func (router *UsersRouter) Read(c *gin.Context) {
// @Produce json
// @Param telegramId path int true "Telegram ID"
// @Success 200 {object} domain.UserResponse
// @Failure 400 {object} domain.UserErrorResponse "invalid telegram id"
// @Failure 404 {object} domain.UserErrorResponse "user not found"
// @Failure 500 {object} domain.UserErrorResponse "internal server error"
// @Failure 400 {object} dto.ErrorResponse "invalid telegram id"
// @Failure 404 {object} dto.ErrorResponse "user not found"
// @Failure 500 {object} dto.ErrorResponse "internal server error"
// @Router /api/v1/users/by-telegram/{telegramId} [get]
func (router *UsersRouter) GetByTelegramID(c *gin.Context) {
var resp domain.UserResponse
telegramID, err := strconv.ParseInt(c.Param("telegramId"), 10, 64)
telegramID, err := requests.ParseInt64(c.Param("telegramId"), "invalid telegram id")
if err != nil {
c.JSON(http.StatusBadRequest, domain.UserErrorResponse{Error: "invalid telegram id"})
c.JSON(http.StatusBadRequest, dto.ErrorResponse{Message: err.Error()})
return
}
user, err := router.service.GetByTelegramID(c.Request.Context(), telegramID)
if err != nil {
handleError(c, err)
handleUserError(c, err)
return
}
@@ -124,27 +124,27 @@ func (router *UsersRouter) GetByTelegramID(c *gin.Context) {
// @Param id path int true "User ID"
// @Param user body domain.UpdateUserRequest true "Данные для обновления"
// @Success 200 {object} domain.UserResponse
// @Failure 400 {object} domain.UserErrorResponse "invalid id or invalid body"
// @Failure 404 {object} domain.UserErrorResponse "user not found"
// @Failure 500 {object} domain.UserErrorResponse "internal server error"
// @Failure 400 {object} dto.ErrorResponse "invalid id or invalid body"
// @Failure 404 {object} dto.ErrorResponse "user not found"
// @Failure 500 {object} dto.ErrorResponse "internal server error"
// @Router /api/v1/users/{id} [patch]
func (router *UsersRouter) Update(c *gin.Context) {
var resp domain.UserResponse
id, err := strconv.ParseInt(c.Param("id"), 10, 64)
id, err := requests.ParseInt64(c.Param("id"), "invalid id")
if err != nil {
c.JSON(http.StatusBadRequest, domain.UserErrorResponse{Error: "invalid id"})
c.JSON(http.StatusBadRequest, dto.ErrorResponse{Message: err.Error()})
return
}
var req domain.UpdateUserRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, domain.UserErrorResponse{Error: err.Error()})
c.JSON(http.StatusBadRequest, dto.ErrorResponse{Message: err.Error()})
return
}
user, err := router.service.Update(c.Request.Context(), id, req)
if err != nil {
handleError(c, err)
handleUserError(c, err)
return
}
@@ -159,35 +159,21 @@ func (router *UsersRouter) Update(c *gin.Context) {
// @Produce json
// @Param id path int true "User ID"
// @Success 204 {string} string "no content"
// @Failure 400 {object} domain.UserErrorResponse "invalid id"
// @Failure 404 {object} domain.UserErrorResponse "user not found"
// @Failure 500 {object} domain.UserErrorResponse "internal server error"
// @Failure 400 {object} dto.ErrorResponse "invalid id"
// @Failure 404 {object} dto.ErrorResponse "user not found"
// @Failure 500 {object} dto.ErrorResponse "internal server error"
// @Router /api/v1/users/{id} [delete]
func (router *UsersRouter) Delete(c *gin.Context) {
id, err := strconv.ParseInt(c.Param("id"), 10, 64)
id, err := requests.ParseInt64(c.Param("id"), "invalid id")
if err != nil {
c.JSON(http.StatusBadRequest, domain.UserErrorResponse{Error: "invalid id"})
c.JSON(http.StatusBadRequest, dto.ErrorResponse{Message: err.Error()})
return
}
if err := router.service.Delete(c.Request.Context(), id); err != nil {
handleError(c, err)
handleUserError(c, err)
return
}
c.Status(http.StatusNoContent)
}
func handleError(c *gin.Context, err error) {
switch {
case errors.Is(err, services.ErrUserNotFound):
c.JSON(http.StatusNotFound, domain.UserErrorResponse{Error: err.Error()})
case errors.Is(err, services.ErrInvalidPatch):
c.JSON(http.StatusBadRequest, domain.UserErrorResponse{Error: err.Error()})
case errors.Is(err, services.ErrTelegramIDMissing):
c.JSON(http.StatusBadRequest, domain.UserErrorResponse{Error: err.Error()})
default:
logInternalError(c, "user request", err)
c.JSON(http.StatusInternalServerError, domain.UserErrorResponse{Error: "internal server error"})
}
}
+2 -2
View File
@@ -99,7 +99,7 @@ func TestUsersRouter_CreateUser(t *testing.T) {
r.ServeHTTP(w, req)
require.Equal(t, http.StatusBadRequest, w.Code)
assert.Contains(t, w.Body.String(), "error")
assert.Contains(t, w.Body.String(), "message")
})
t.Run("bad request on domain validation error", func(t *testing.T) {
@@ -227,7 +227,7 @@ func TestUsersRouter_Update(t *testing.T) {
r.ServeHTTP(w, req)
require.Equal(t, http.StatusBadRequest, w.Code)
assert.Contains(t, w.Body.String(), "error")
assert.Contains(t, w.Body.String(), "message")
})
t.Run("bad request on invalid patch", func(t *testing.T) {