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
+34 -246
View File
@@ -110,19 +110,13 @@ const docTemplate = `{
"400": {
"description": "invalid body",
"schema": {
"type": "object",
"additionalProperties": {
"type": "string"
}
"$ref": "#/definitions/dto.ErrorResponse"
}
},
"500": {
"description": "internal server error",
"schema": {
"type": "object",
"additionalProperties": {
"type": "string"
}
"$ref": "#/definitions/dto.ErrorResponse"
}
}
}
@@ -160,28 +154,19 @@ const docTemplate = `{
"400": {
"description": "invalid id",
"schema": {
"type": "object",
"additionalProperties": {
"type": "string"
}
"$ref": "#/definitions/dto.ErrorResponse"
}
},
"404": {
"description": "family not found",
"schema": {
"type": "object",
"additionalProperties": {
"type": "string"
}
"$ref": "#/definitions/dto.ErrorResponse"
}
},
"500": {
"description": "internal server error",
"schema": {
"type": "object",
"additionalProperties": {
"type": "string"
}
"$ref": "#/definitions/dto.ErrorResponse"
}
}
}
@@ -217,28 +202,19 @@ const docTemplate = `{
"400": {
"description": "invalid id",
"schema": {
"type": "object",
"additionalProperties": {
"type": "string"
}
"$ref": "#/definitions/dto.ErrorResponse"
}
},
"404": {
"description": "family not found",
"schema": {
"type": "object",
"additionalProperties": {
"type": "string"
}
"$ref": "#/definitions/dto.ErrorResponse"
}
},
"500": {
"description": "internal server error",
"schema": {
"type": "object",
"additionalProperties": {
"type": "string"
}
"$ref": "#/definitions/dto.ErrorResponse"
}
}
}
@@ -283,146 +259,17 @@ const docTemplate = `{
"400": {
"description": "name is required",
"schema": {
"type": "object",
"additionalProperties": {
"type": "string"
}
"$ref": "#/definitions/dto.ErrorResponse"
}
},
"404": {
"description": "family not found",
"schema": {
"type": "object",
"additionalProperties": {
"type": "string"
}
"$ref": "#/definitions/dto.ErrorResponse"
}
},
"500": {
"description": "internal server error",
"schema": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
}
}
}
},
"/api/v1/receipts": {
"post": {
"description": "Загружает чек из внешнего сервиса и опционально автоматически создает связанную транзакцию",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"Receipts"
],
"summary": "Загрузить чек",
"parameters": [
{
"description": "Receipt payload",
"name": "receipt",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/domain.AddReceiptRequest"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/domain.AddReceiptResponse"
}
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/dto.ErrorResponse"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/dto.ErrorResponse"
}
}
}
}
},
"/api/v1/receipts/photo": {
"post": {
"description": "Принимает фото, распознает текст через Google OCR и создает чек с позициями; опционально создает связанную транзакцию",
"consumes": [
"multipart/form-data"
],
"produces": [
"application/json"
],
"tags": [
"Receipts"
],
"summary": "Загрузить чек по фото",
"parameters": [
{
"type": "file",
"description": "Receipt photo",
"name": "photo",
"in": "formData",
"required": true
},
{
"type": "integer",
"description": "Family ID for auto-created transaction",
"name": "family_id",
"in": "formData"
},
{
"type": "integer",
"description": "User ID for auto-created transaction",
"name": "created_by",
"in": "formData"
},
{
"type": "string",
"description": "Transaction type, default expense",
"name": "type",
"in": "formData"
},
{
"type": "string",
"description": "Transaction category, default receipt",
"name": "category",
"in": "formData"
},
{
"type": "string",
"description": "Transaction description override",
"name": "description",
"in": "formData"
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/domain.AddReceiptResponse"
}
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/dto.ErrorResponse"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/dto.ErrorResponse"
}
@@ -515,9 +362,10 @@ const docTemplate = `{
}
},
"post": {
"description": "Создает новую транзакцию и при необходимости привязывает к ней чек",
"description": "Создает транзакцию одним из трех способов.\n1. application/json: ручная транзакция с полями family_id, created_by, type, category, amount, datetime.\n2. application/json: транзакция по чеку с полями family_id, created_by, receipt_number, receipt_date.\n3. multipart/form-data: транзакция по фото чека с полями photo, family_id, created_by и опциональными type, category, description.\nВ одном JSON-запросе нельзя смешивать ручные поля транзакции с полями receipt_number и receipt_date.",
"consumes": [
"application/json"
"application/json",
"multipart/form-data"
],
"produces": [
"application/json"
@@ -528,10 +376,9 @@ const docTemplate = `{
"summary": "Создать транзакцию",
"parameters": [
{
"description": "Transaction payload",
"description": "JSON payload for manual or receipt-based transaction creation",
"name": "transaction",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/dto.CreateTransactionRequest"
}
@@ -816,13 +663,13 @@ const docTemplate = `{
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/domain.UserErrorResponse"
"$ref": "#/definitions/dto.ErrorResponse"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/domain.UserErrorResponse"
"$ref": "#/definitions/dto.ErrorResponse"
}
}
}
@@ -860,19 +707,19 @@ const docTemplate = `{
"400": {
"description": "invalid telegram id",
"schema": {
"$ref": "#/definitions/domain.UserErrorResponse"
"$ref": "#/definitions/dto.ErrorResponse"
}
},
"404": {
"description": "user not found",
"schema": {
"$ref": "#/definitions/domain.UserErrorResponse"
"$ref": "#/definitions/dto.ErrorResponse"
}
},
"500": {
"description": "internal server error",
"schema": {
"$ref": "#/definitions/domain.UserErrorResponse"
"$ref": "#/definitions/dto.ErrorResponse"
}
}
}
@@ -910,19 +757,19 @@ const docTemplate = `{
"400": {
"description": "invalid id",
"schema": {
"$ref": "#/definitions/domain.UserErrorResponse"
"$ref": "#/definitions/dto.ErrorResponse"
}
},
"404": {
"description": "user not found",
"schema": {
"$ref": "#/definitions/domain.UserErrorResponse"
"$ref": "#/definitions/dto.ErrorResponse"
}
},
"500": {
"description": "internal server error",
"schema": {
"$ref": "#/definitions/domain.UserErrorResponse"
"$ref": "#/definitions/dto.ErrorResponse"
}
}
}
@@ -958,19 +805,19 @@ const docTemplate = `{
"400": {
"description": "invalid id",
"schema": {
"$ref": "#/definitions/domain.UserErrorResponse"
"$ref": "#/definitions/dto.ErrorResponse"
}
},
"404": {
"description": "user not found",
"schema": {
"$ref": "#/definitions/domain.UserErrorResponse"
"$ref": "#/definitions/dto.ErrorResponse"
}
},
"500": {
"description": "internal server error",
"schema": {
"$ref": "#/definitions/domain.UserErrorResponse"
"$ref": "#/definitions/dto.ErrorResponse"
}
}
}
@@ -1015,19 +862,19 @@ const docTemplate = `{
"400": {
"description": "invalid id or invalid body",
"schema": {
"$ref": "#/definitions/domain.UserErrorResponse"
"$ref": "#/definitions/dto.ErrorResponse"
}
},
"404": {
"description": "user not found",
"schema": {
"$ref": "#/definitions/domain.UserErrorResponse"
"$ref": "#/definitions/dto.ErrorResponse"
}
},
"500": {
"description": "internal server error",
"schema": {
"$ref": "#/definitions/domain.UserErrorResponse"
"$ref": "#/definitions/dto.ErrorResponse"
}
}
}
@@ -1035,55 +882,6 @@ const docTemplate = `{
}
},
"definitions": {
"domain.AddReceiptRequest": {
"type": "object",
"required": [
"date",
"number"
],
"properties": {
"category": {
"type": "string"
},
"created_by": {
"type": "integer"
},
"date": {
"type": "string"
},
"description": {
"type": "string"
},
"family_id": {
"type": "integer"
},
"number": {
"type": "string",
"maxLength": 24,
"minLength": 24
},
"type": {
"type": "string"
}
}
},
"domain.AddReceiptResponse": {
"type": "object",
"properties": {
"date": {
"type": "string"
},
"id": {
"type": "integer"
},
"number": {
"type": "string"
},
"transaction_id": {
"type": "integer"
}
}
},
"domain.CreateFamilyRequest": {
"type": "object",
"properties": {
@@ -1182,14 +980,6 @@ const docTemplate = `{
}
}
},
"domain.UserErrorResponse": {
"type": "object",
"properties": {
"error": {
"type": "string"
}
}
},
"domain.UserResponse": {
"type": "object",
"properties": {
@@ -1267,14 +1057,6 @@ const docTemplate = `{
},
"dto.CreateTransactionRequest": {
"type": "object",
"required": [
"amount",
"category",
"created_by",
"datetime",
"family_id",
"type"
],
"properties": {
"amount": {
"type": "number"
@@ -1294,9 +1076,15 @@ const docTemplate = `{
"family_id": {
"type": "integer"
},
"receipt_date": {
"type": "string"
},
"receipt_id": {
"type": "integer"
},
"receipt_number": {
"type": "string"
},
"type": {
"type": "string"
}
+34 -246
View File
@@ -99,19 +99,13 @@
"400": {
"description": "invalid body",
"schema": {
"type": "object",
"additionalProperties": {
"type": "string"
}
"$ref": "#/definitions/dto.ErrorResponse"
}
},
"500": {
"description": "internal server error",
"schema": {
"type": "object",
"additionalProperties": {
"type": "string"
}
"$ref": "#/definitions/dto.ErrorResponse"
}
}
}
@@ -149,28 +143,19 @@
"400": {
"description": "invalid id",
"schema": {
"type": "object",
"additionalProperties": {
"type": "string"
}
"$ref": "#/definitions/dto.ErrorResponse"
}
},
"404": {
"description": "family not found",
"schema": {
"type": "object",
"additionalProperties": {
"type": "string"
}
"$ref": "#/definitions/dto.ErrorResponse"
}
},
"500": {
"description": "internal server error",
"schema": {
"type": "object",
"additionalProperties": {
"type": "string"
}
"$ref": "#/definitions/dto.ErrorResponse"
}
}
}
@@ -206,28 +191,19 @@
"400": {
"description": "invalid id",
"schema": {
"type": "object",
"additionalProperties": {
"type": "string"
}
"$ref": "#/definitions/dto.ErrorResponse"
}
},
"404": {
"description": "family not found",
"schema": {
"type": "object",
"additionalProperties": {
"type": "string"
}
"$ref": "#/definitions/dto.ErrorResponse"
}
},
"500": {
"description": "internal server error",
"schema": {
"type": "object",
"additionalProperties": {
"type": "string"
}
"$ref": "#/definitions/dto.ErrorResponse"
}
}
}
@@ -272,146 +248,17 @@
"400": {
"description": "name is required",
"schema": {
"type": "object",
"additionalProperties": {
"type": "string"
}
"$ref": "#/definitions/dto.ErrorResponse"
}
},
"404": {
"description": "family not found",
"schema": {
"type": "object",
"additionalProperties": {
"type": "string"
}
"$ref": "#/definitions/dto.ErrorResponse"
}
},
"500": {
"description": "internal server error",
"schema": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
}
}
}
},
"/api/v1/receipts": {
"post": {
"description": "Загружает чек из внешнего сервиса и опционально автоматически создает связанную транзакцию",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"Receipts"
],
"summary": "Загрузить чек",
"parameters": [
{
"description": "Receipt payload",
"name": "receipt",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/domain.AddReceiptRequest"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/domain.AddReceiptResponse"
}
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/dto.ErrorResponse"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/dto.ErrorResponse"
}
}
}
}
},
"/api/v1/receipts/photo": {
"post": {
"description": "Принимает фото, распознает текст через Google OCR и создает чек с позициями; опционально создает связанную транзакцию",
"consumes": [
"multipart/form-data"
],
"produces": [
"application/json"
],
"tags": [
"Receipts"
],
"summary": "Загрузить чек по фото",
"parameters": [
{
"type": "file",
"description": "Receipt photo",
"name": "photo",
"in": "formData",
"required": true
},
{
"type": "integer",
"description": "Family ID for auto-created transaction",
"name": "family_id",
"in": "formData"
},
{
"type": "integer",
"description": "User ID for auto-created transaction",
"name": "created_by",
"in": "formData"
},
{
"type": "string",
"description": "Transaction type, default expense",
"name": "type",
"in": "formData"
},
{
"type": "string",
"description": "Transaction category, default receipt",
"name": "category",
"in": "formData"
},
{
"type": "string",
"description": "Transaction description override",
"name": "description",
"in": "formData"
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/domain.AddReceiptResponse"
}
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/dto.ErrorResponse"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/dto.ErrorResponse"
}
@@ -504,9 +351,10 @@
}
},
"post": {
"description": "Создает новую транзакцию и при необходимости привязывает к ней чек",
"description": "Создает транзакцию одним из трех способов.\n1. application/json: ручная транзакция с полями family_id, created_by, type, category, amount, datetime.\n2. application/json: транзакция по чеку с полями family_id, created_by, receipt_number, receipt_date.\n3. multipart/form-data: транзакция по фото чека с полями photo, family_id, created_by и опциональными type, category, description.\nВ одном JSON-запросе нельзя смешивать ручные поля транзакции с полями receipt_number и receipt_date.",
"consumes": [
"application/json"
"application/json",
"multipart/form-data"
],
"produces": [
"application/json"
@@ -517,10 +365,9 @@
"summary": "Создать транзакцию",
"parameters": [
{
"description": "Transaction payload",
"description": "JSON payload for manual or receipt-based transaction creation",
"name": "transaction",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/dto.CreateTransactionRequest"
}
@@ -805,13 +652,13 @@
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/domain.UserErrorResponse"
"$ref": "#/definitions/dto.ErrorResponse"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/domain.UserErrorResponse"
"$ref": "#/definitions/dto.ErrorResponse"
}
}
}
@@ -849,19 +696,19 @@
"400": {
"description": "invalid telegram id",
"schema": {
"$ref": "#/definitions/domain.UserErrorResponse"
"$ref": "#/definitions/dto.ErrorResponse"
}
},
"404": {
"description": "user not found",
"schema": {
"$ref": "#/definitions/domain.UserErrorResponse"
"$ref": "#/definitions/dto.ErrorResponse"
}
},
"500": {
"description": "internal server error",
"schema": {
"$ref": "#/definitions/domain.UserErrorResponse"
"$ref": "#/definitions/dto.ErrorResponse"
}
}
}
@@ -899,19 +746,19 @@
"400": {
"description": "invalid id",
"schema": {
"$ref": "#/definitions/domain.UserErrorResponse"
"$ref": "#/definitions/dto.ErrorResponse"
}
},
"404": {
"description": "user not found",
"schema": {
"$ref": "#/definitions/domain.UserErrorResponse"
"$ref": "#/definitions/dto.ErrorResponse"
}
},
"500": {
"description": "internal server error",
"schema": {
"$ref": "#/definitions/domain.UserErrorResponse"
"$ref": "#/definitions/dto.ErrorResponse"
}
}
}
@@ -947,19 +794,19 @@
"400": {
"description": "invalid id",
"schema": {
"$ref": "#/definitions/domain.UserErrorResponse"
"$ref": "#/definitions/dto.ErrorResponse"
}
},
"404": {
"description": "user not found",
"schema": {
"$ref": "#/definitions/domain.UserErrorResponse"
"$ref": "#/definitions/dto.ErrorResponse"
}
},
"500": {
"description": "internal server error",
"schema": {
"$ref": "#/definitions/domain.UserErrorResponse"
"$ref": "#/definitions/dto.ErrorResponse"
}
}
}
@@ -1004,19 +851,19 @@
"400": {
"description": "invalid id or invalid body",
"schema": {
"$ref": "#/definitions/domain.UserErrorResponse"
"$ref": "#/definitions/dto.ErrorResponse"
}
},
"404": {
"description": "user not found",
"schema": {
"$ref": "#/definitions/domain.UserErrorResponse"
"$ref": "#/definitions/dto.ErrorResponse"
}
},
"500": {
"description": "internal server error",
"schema": {
"$ref": "#/definitions/domain.UserErrorResponse"
"$ref": "#/definitions/dto.ErrorResponse"
}
}
}
@@ -1024,55 +871,6 @@
}
},
"definitions": {
"domain.AddReceiptRequest": {
"type": "object",
"required": [
"date",
"number"
],
"properties": {
"category": {
"type": "string"
},
"created_by": {
"type": "integer"
},
"date": {
"type": "string"
},
"description": {
"type": "string"
},
"family_id": {
"type": "integer"
},
"number": {
"type": "string",
"maxLength": 24,
"minLength": 24
},
"type": {
"type": "string"
}
}
},
"domain.AddReceiptResponse": {
"type": "object",
"properties": {
"date": {
"type": "string"
},
"id": {
"type": "integer"
},
"number": {
"type": "string"
},
"transaction_id": {
"type": "integer"
}
}
},
"domain.CreateFamilyRequest": {
"type": "object",
"properties": {
@@ -1171,14 +969,6 @@
}
}
},
"domain.UserErrorResponse": {
"type": "object",
"properties": {
"error": {
"type": "string"
}
}
},
"domain.UserResponse": {
"type": "object",
"properties": {
@@ -1256,14 +1046,6 @@
},
"dto.CreateTransactionRequest": {
"type": "object",
"required": [
"amount",
"category",
"created_by",
"datetime",
"family_id",
"type"
],
"properties": {
"amount": {
"type": "number"
@@ -1283,9 +1065,15 @@
"family_id": {
"type": "integer"
},
"receipt_date": {
"type": "string"
},
"receipt_id": {
"type": "integer"
},
"receipt_number": {
"type": "string"
},
"type": {
"type": "string"
}
+37 -177
View File
@@ -1,37 +1,4 @@
definitions:
domain.AddReceiptRequest:
properties:
category:
type: string
created_by:
type: integer
date:
type: string
description:
type: string
family_id:
type: integer
number:
maxLength: 24
minLength: 24
type: string
type:
type: string
required:
- date
- number
type: object
domain.AddReceiptResponse:
properties:
date:
type: string
id:
type: integer
number:
type: string
transaction_id:
type: integer
type: object
domain.CreateFamilyRequest:
properties:
name:
@@ -96,11 +63,6 @@ definitions:
username:
type: string
type: object
domain.UserErrorResponse:
properties:
error:
type: string
type: object
domain.UserResponse:
properties:
created_at:
@@ -164,17 +126,14 @@ definitions:
type: string
family_id:
type: integer
receipt_date:
type: string
receipt_id:
type: integer
receipt_number:
type: string
type:
type: string
required:
- amount
- category
- created_by
- datetime
- family_id
- type
type: object
dto.ErrorResponse:
properties:
@@ -302,15 +261,11 @@ paths:
"400":
description: invalid body
schema:
additionalProperties:
type: string
type: object
$ref: '#/definitions/dto.ErrorResponse'
"500":
description: internal server error
schema:
additionalProperties:
type: string
type: object
$ref: '#/definitions/dto.ErrorResponse'
summary: Создать семью
tags:
- Families
@@ -335,21 +290,15 @@ paths:
"400":
description: invalid id
schema:
additionalProperties:
type: string
type: object
$ref: '#/definitions/dto.ErrorResponse'
"404":
description: family not found
schema:
additionalProperties:
type: string
type: object
$ref: '#/definitions/dto.ErrorResponse'
"500":
description: internal server error
schema:
additionalProperties:
type: string
type: object
$ref: '#/definitions/dto.ErrorResponse'
summary: Удалить семью
tags:
- Families
@@ -373,21 +322,15 @@ paths:
"400":
description: invalid id
schema:
additionalProperties:
type: string
type: object
$ref: '#/definitions/dto.ErrorResponse'
"404":
description: family not found
schema:
additionalProperties:
type: string
type: object
$ref: '#/definitions/dto.ErrorResponse'
"500":
description: internal server error
schema:
additionalProperties:
type: string
type: object
$ref: '#/definitions/dto.ErrorResponse'
summary: Получить семью по ID
tags:
- Families
@@ -417,105 +360,18 @@ paths:
"400":
description: name is required
schema:
additionalProperties:
type: string
type: object
$ref: '#/definitions/dto.ErrorResponse'
"404":
description: family not found
schema:
additionalProperties:
type: string
type: object
$ref: '#/definitions/dto.ErrorResponse'
"500":
description: internal server error
schema:
additionalProperties:
type: string
type: object
$ref: '#/definitions/dto.ErrorResponse'
summary: Обновить семью
tags:
- Families
/api/v1/receipts:
post:
consumes:
- application/json
description: Загружает чек из внешнего сервиса и опционально автоматически создает
связанную транзакцию
parameters:
- description: Receipt payload
in: body
name: receipt
required: true
schema:
$ref: '#/definitions/domain.AddReceiptRequest'
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/domain.AddReceiptResponse'
"400":
description: Bad Request
schema:
$ref: '#/definitions/dto.ErrorResponse'
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/dto.ErrorResponse'
summary: Загрузить чек
tags:
- Receipts
/api/v1/receipts/photo:
post:
consumes:
- multipart/form-data
description: Принимает фото, распознает текст через Google OCR и создает чек
с позициями; опционально создает связанную транзакцию
parameters:
- description: Receipt photo
in: formData
name: photo
required: true
type: file
- description: Family ID for auto-created transaction
in: formData
name: family_id
type: integer
- description: User ID for auto-created transaction
in: formData
name: created_by
type: integer
- description: Transaction type, default expense
in: formData
name: type
type: string
- description: Transaction category, default receipt
in: formData
name: category
type: string
- description: Transaction description override
in: formData
name: description
type: string
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/domain.AddReceiptResponse'
"400":
description: Bad Request
schema:
$ref: '#/definitions/dto.ErrorResponse'
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/dto.ErrorResponse'
summary: Загрузить чек по фото
tags:
- Receipts
/api/v1/transactions:
get:
consumes:
@@ -575,13 +431,17 @@ paths:
post:
consumes:
- application/json
description: Создает новую транзакцию и при необходимости привязывает к ней
чек
- multipart/form-data
description: |-
Создает транзакцию одним из трех способов.
1. application/json: ручная транзакция с полями family_id, created_by, type, category, amount, datetime.
2. application/json: транзакция по чеку с полями family_id, created_by, receipt_number, receipt_date.
3. multipart/form-data: транзакция по фото чека с полями photo, family_id, created_by и опциональными type, category, description.
В одном JSON-запросе нельзя смешивать ручные поля транзакции с полями receipt_number и receipt_date.
parameters:
- description: Transaction payload
- description: JSON payload for manual or receipt-based transaction creation
in: body
name: transaction
required: true
schema:
$ref: '#/definitions/dto.CreateTransactionRequest'
produces:
@@ -773,11 +633,11 @@ paths:
"400":
description: Bad Request
schema:
$ref: '#/definitions/domain.UserErrorResponse'
$ref: '#/definitions/dto.ErrorResponse'
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/domain.UserErrorResponse'
$ref: '#/definitions/dto.ErrorResponse'
summary: Создать пользователя
tags:
- Users
@@ -802,15 +662,15 @@ paths:
"400":
description: invalid id
schema:
$ref: '#/definitions/domain.UserErrorResponse'
$ref: '#/definitions/dto.ErrorResponse'
"404":
description: user not found
schema:
$ref: '#/definitions/domain.UserErrorResponse'
$ref: '#/definitions/dto.ErrorResponse'
"500":
description: internal server error
schema:
$ref: '#/definitions/domain.UserErrorResponse'
$ref: '#/definitions/dto.ErrorResponse'
summary: Удалить пользователя
tags:
- Users
@@ -834,15 +694,15 @@ paths:
"400":
description: invalid id
schema:
$ref: '#/definitions/domain.UserErrorResponse'
$ref: '#/definitions/dto.ErrorResponse'
"404":
description: user not found
schema:
$ref: '#/definitions/domain.UserErrorResponse'
$ref: '#/definitions/dto.ErrorResponse'
"500":
description: internal server error
schema:
$ref: '#/definitions/domain.UserErrorResponse'
$ref: '#/definitions/dto.ErrorResponse'
summary: Получить пользователя по ID
tags:
- Users
@@ -872,15 +732,15 @@ paths:
"400":
description: invalid id or invalid body
schema:
$ref: '#/definitions/domain.UserErrorResponse'
$ref: '#/definitions/dto.ErrorResponse'
"404":
description: user not found
schema:
$ref: '#/definitions/domain.UserErrorResponse'
$ref: '#/definitions/dto.ErrorResponse'
"500":
description: internal server error
schema:
$ref: '#/definitions/domain.UserErrorResponse'
$ref: '#/definitions/dto.ErrorResponse'
summary: Обновить пользователя
tags:
- Users
@@ -905,15 +765,15 @@ paths:
"400":
description: invalid telegram id
schema:
$ref: '#/definitions/domain.UserErrorResponse'
$ref: '#/definitions/dto.ErrorResponse'
"404":
description: user not found
schema:
$ref: '#/definitions/domain.UserErrorResponse'
$ref: '#/definitions/dto.ErrorResponse'
"500":
description: internal server error
schema:
$ref: '#/definitions/domain.UserErrorResponse'
$ref: '#/definitions/dto.ErrorResponse'
summary: Получить пользователя по Telegram ID
tags:
- Users
+15
View File
@@ -0,0 +1,15 @@
package requests
import (
"FamilyHub/src/api/dto"
"FamilyHub/src/domain"
)
func BuildActivityListFilter(query dto.ActivityListQuery) domain.ActivityLogListFilter {
return domain.ActivityLogListFilter{
FamilyID: query.FamilyID,
UserID: query.UserID,
Limit: query.Limit,
Offset: query.Offset,
}
}
+16
View File
@@ -0,0 +1,16 @@
package requests
import (
"errors"
"strconv"
"strings"
)
func ParseInt64(value string, invalidMessage string) (int64, error) {
parsed, err := strconv.ParseInt(strings.TrimSpace(value), 10, 64)
if err != nil {
return 0, errors.New(invalidMessage)
}
return parsed, nil
}
+16
View File
@@ -0,0 +1,16 @@
package requests
import (
"FamilyHub/src/domain"
"errors"
)
var ErrFamilyNameRequired = errors.New("name is required")
func ValidateFamilyUpdate(req domain.UpdateFamilyRequest) error {
if req.Name == nil {
return ErrFamilyNameRequired
}
return nil
}
+16 -42
View File
@@ -6,13 +6,19 @@ import (
"FamilyHub/src/domain"
"FamilyHub/src/utils"
"errors"
"strconv"
"strings"
"time"
"github.com/gin-gonic/gin"
)
type PhotoCreateTransactionFields struct {
Image []byte
FamilyID *int64
CreatedBy *int64
Type *string
Category *string
Description *string
}
func BuildCreateTransactionInput(req dto.CreateTransactionRequest) (services.CreateTransactionInput, error) {
if req.ReceiptNumber != nil || req.ReceiptDate != nil {
receiptReq, err := BuildReceiptTransactionRequest(req)
@@ -30,24 +36,15 @@ func BuildCreateTransactionInput(req dto.CreateTransactionRequest) (services.Cre
return services.CreateTransactionInput{Manual: &manualReq}, nil
}
func BuildPhotoCreateTransactionInput(c *gin.Context, image []byte) (services.CreateTransactionInput, error) {
familyID, err := parseOptionalInt64Form(c, "family_id")
if err != nil {
return services.CreateTransactionInput{}, err
}
createdBy, err := parseOptionalInt64Form(c, "created_by")
if err != nil {
return services.CreateTransactionInput{}, err
}
func BuildPhotoCreateTransactionInput(fields PhotoCreateTransactionFields) (services.CreateTransactionInput, error) {
return services.CreateTransactionInput{
Photo: &services.CreateTransactionPhotoInput{
Image: image,
FamilyID: familyID,
CreatedBy: createdBy,
Type: parseOptionalStringForm(c, "type"),
Category: parseOptionalStringForm(c, "category"),
Description: parseOptionalStringForm(c, "description"),
Image: fields.Image,
FamilyID: fields.FamilyID,
CreatedBy: fields.CreatedBy,
Type: trimOptionalString(fields.Type),
Category: trimOptionalString(fields.Category),
Description: trimOptionalString(fields.Description),
},
}, nil
}
@@ -128,26 +125,3 @@ func trimOptionalString(value *string) *string {
return &trimmed
}
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
}
+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) {
-4
View File
@@ -41,10 +41,6 @@ type UserResponse struct {
UpdatedAt string `json:"updated_at"`
}
type UserErrorResponse struct {
Error string `json:"error"`
}
func (response *UserResponse) ModelToResponse(u *UserModel) UserResponse {
return UserResponse{
ID: u.ID,