Refactored transaction input handling and removed unused receipt-related definitions in Swagger.
This commit is contained in:
+34
-246
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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"})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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"})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -7,9 +7,10 @@
|
||||
- расходов
|
||||
- категорий
|
||||
|
||||
Поддерживает два способа ввода расходов:
|
||||
Поддерживает три способа ввода расходов:
|
||||
1. Ручной ввод
|
||||
2. Сканирование чека (QR-код)
|
||||
2. Ввод номера и даты чека
|
||||
3. Загрузка фото чека
|
||||
|
||||
---
|
||||
|
||||
@@ -155,13 +156,27 @@ categories (
|
||||
|
||||
---
|
||||
|
||||
### 4.3 Сканирование чека
|
||||
### 4.3 Добавление расхода по номеру и дате чека
|
||||
|
||||
#### Поток:
|
||||
1. Пользователь отправляет QR-код
|
||||
1. Пользователь вводит номер и дату чека
|
||||
2. Backend получает данные чека через внешний сервис
|
||||
3. Создаётся запись в `receipts`
|
||||
4. Для каждой позиции создаётся запись в `positions`
|
||||
4. Создаётся связанная транзакция в `transactions`
|
||||
5. Для каждой позиции создаётся запись в `positions`
|
||||
|
||||
---
|
||||
|
||||
### 4.4 Добавление расхода по фото чека
|
||||
|
||||
#### Поток:
|
||||
1. Пользователь загружает фото чека
|
||||
2. Backend извлекает текст через OCR
|
||||
3. Backend извлекает из текста номер и дату чека
|
||||
4. Backend получает данные чека через внешний сервис
|
||||
5. Создаётся запись в `receipts`
|
||||
6. Создаётся связанная транзакция в `transactions`
|
||||
7. Для каждой позиции создаётся запись в `positions`
|
||||
|
||||
---
|
||||
|
||||
@@ -169,54 +184,92 @@ categories (
|
||||
|
||||
### Ручной ввод
|
||||
|
||||
```
|
||||
User → API → positions
|
||||
```text
|
||||
User → API POST /transactions → transactions
|
||||
```
|
||||
|
||||
### Доход
|
||||
|
||||
```text
|
||||
User → API POST /transactions → transactions
|
||||
```
|
||||
User → API → positions
|
||||
|
||||
### Чек по номеру и дате
|
||||
|
||||
```text
|
||||
User → API POST /transactions → receipt provider → receipts + positions + transactions
|
||||
```
|
||||
|
||||
### Чек по фото
|
||||
|
||||
```text
|
||||
User → API POST /transactions multipart/form-data → OCR → receipt provider → receipts + positions + transactions
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. API (черновик)
|
||||
|
||||
### Создание позиции
|
||||
### Создание транзакции вручную
|
||||
|
||||
```
|
||||
POST /positions
|
||||
```http
|
||||
POST /api/v1/transactions
|
||||
Content-Type: application/json
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"family_id": 1,
|
||||
"created_by": 2,
|
||||
"type": "expense",
|
||||
"category": "groceries",
|
||||
"amount": 1000,
|
||||
"category_id": 1,
|
||||
"description": "Продукты"
|
||||
"description": "Продукты",
|
||||
"datetime": "2026-01-21T10:11:12Z"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Сканирование чека
|
||||
### Создание транзакции по номеру и дате чека
|
||||
|
||||
```
|
||||
POST /receipts/scan
|
||||
```http
|
||||
POST /api/v1/transactions
|
||||
Content-Type: application/json
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"qr_data": "string"
|
||||
"family_id": 1,
|
||||
"created_by": 2,
|
||||
"receipt_number": "0123456789ABCDEFGHIJKLMN",
|
||||
"receipt_date": "21.01.2026"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Получение позиций
|
||||
### Создание транзакции по фото чека
|
||||
|
||||
```http
|
||||
POST /api/v1/transactions
|
||||
Content-Type: multipart/form-data
|
||||
```
|
||||
GET /positions
|
||||
|
||||
Поля формы:
|
||||
- `photo` — файл изображения чека, обязательно
|
||||
- `family_id` — ID семьи, обязательно
|
||||
- `created_by` — ID пользователя, обязательно
|
||||
- `type` — тип транзакции, опционально, по умолчанию `expense`
|
||||
- `category` — категория, опционально, по умолчанию `receipt`
|
||||
- `description` — описание транзакции, опционально
|
||||
|
||||
---
|
||||
|
||||
### Получение транзакций
|
||||
|
||||
```http
|
||||
GET /api/v1/transactions
|
||||
```
|
||||
|
||||
Фильтры:
|
||||
@@ -227,6 +280,21 @@ GET /positions
|
||||
|
||||
---
|
||||
|
||||
### Правила для `POST /api/v1/transactions`
|
||||
|
||||
Используется ровно один сценарий создания:
|
||||
|
||||
1. Ручная транзакция:
|
||||
обязательны `family_id`, `created_by`, `type`, `category`, `amount`, `datetime`
|
||||
2. Транзакция по чеку:
|
||||
обязательны `family_id`, `created_by`, `receipt_number`, `receipt_date`
|
||||
3. Транзакция по фото:
|
||||
обязательны `photo`, `family_id`, `created_by`
|
||||
|
||||
Нельзя смешивать ручные поля транзакции (`amount`, `datetime`, `receipt_id`) с полями чека (`receipt_number`, `receipt_date`) в одном JSON-запросе.
|
||||
|
||||
---
|
||||
|
||||
## 7. Задачи для разработки
|
||||
|
||||
### Этап 1 — База
|
||||
@@ -242,10 +310,10 @@ GET /positions
|
||||
|
||||
---
|
||||
|
||||
### Этап 3 — Позиции
|
||||
### Этап 3 — Транзакции
|
||||
|
||||
- [ ] Endpoint создания позиции
|
||||
- [ ] Endpoint получения списка
|
||||
- [x] Endpoint создания транзакции
|
||||
- [x] Endpoint получения списка
|
||||
- [ ] Фильтрация
|
||||
|
||||
---
|
||||
@@ -259,9 +327,11 @@ GET /positions
|
||||
|
||||
### Этап 5 — Чеки
|
||||
|
||||
- [ ] Endpoint загрузки QR
|
||||
- [x] Endpoint загрузки фото чека через `POST /transactions`
|
||||
- [ ] Интеграция с сервисом чеков
|
||||
- [ ] Создание receipts
|
||||
- [x] Создание receipts
|
||||
- [x] Создание транзакции по номеру и дате чека
|
||||
- [x] Создание транзакции по фото чека
|
||||
- [ ] Создание positions
|
||||
|
||||
---
|
||||
@@ -295,5 +365,5 @@ GET /positions
|
||||
- [ ] Нужна ли мультивалютность?
|
||||
- [ ] Можно ли редактировать чек?
|
||||
- [ ] Как обрабатывать ошибки OCR?
|
||||
- [ ] Нужен ли отдельный endpoint для повторной привязки чека к существующей транзакции?
|
||||
- [ ] Нужны ли роли внутри семьи?
|
||||
|
||||
|
||||
Reference in New Issue
Block a user