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": {
|
"400": {
|
||||||
"description": "invalid body",
|
"description": "invalid body",
|
||||||
"schema": {
|
"schema": {
|
||||||
"type": "object",
|
"$ref": "#/definitions/dto.ErrorResponse"
|
||||||
"additionalProperties": {
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"500": {
|
"500": {
|
||||||
"description": "internal server error",
|
"description": "internal server error",
|
||||||
"schema": {
|
"schema": {
|
||||||
"type": "object",
|
"$ref": "#/definitions/dto.ErrorResponse"
|
||||||
"additionalProperties": {
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -160,28 +154,19 @@ const docTemplate = `{
|
|||||||
"400": {
|
"400": {
|
||||||
"description": "invalid id",
|
"description": "invalid id",
|
||||||
"schema": {
|
"schema": {
|
||||||
"type": "object",
|
"$ref": "#/definitions/dto.ErrorResponse"
|
||||||
"additionalProperties": {
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"404": {
|
"404": {
|
||||||
"description": "family not found",
|
"description": "family not found",
|
||||||
"schema": {
|
"schema": {
|
||||||
"type": "object",
|
"$ref": "#/definitions/dto.ErrorResponse"
|
||||||
"additionalProperties": {
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"500": {
|
"500": {
|
||||||
"description": "internal server error",
|
"description": "internal server error",
|
||||||
"schema": {
|
"schema": {
|
||||||
"type": "object",
|
"$ref": "#/definitions/dto.ErrorResponse"
|
||||||
"additionalProperties": {
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -217,28 +202,19 @@ const docTemplate = `{
|
|||||||
"400": {
|
"400": {
|
||||||
"description": "invalid id",
|
"description": "invalid id",
|
||||||
"schema": {
|
"schema": {
|
||||||
"type": "object",
|
"$ref": "#/definitions/dto.ErrorResponse"
|
||||||
"additionalProperties": {
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"404": {
|
"404": {
|
||||||
"description": "family not found",
|
"description": "family not found",
|
||||||
"schema": {
|
"schema": {
|
||||||
"type": "object",
|
"$ref": "#/definitions/dto.ErrorResponse"
|
||||||
"additionalProperties": {
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"500": {
|
"500": {
|
||||||
"description": "internal server error",
|
"description": "internal server error",
|
||||||
"schema": {
|
"schema": {
|
||||||
"type": "object",
|
"$ref": "#/definitions/dto.ErrorResponse"
|
||||||
"additionalProperties": {
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -283,146 +259,17 @@ const docTemplate = `{
|
|||||||
"400": {
|
"400": {
|
||||||
"description": "name is required",
|
"description": "name is required",
|
||||||
"schema": {
|
"schema": {
|
||||||
"type": "object",
|
"$ref": "#/definitions/dto.ErrorResponse"
|
||||||
"additionalProperties": {
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"404": {
|
"404": {
|
||||||
"description": "family not found",
|
"description": "family not found",
|
||||||
"schema": {
|
"schema": {
|
||||||
"type": "object",
|
"$ref": "#/definitions/dto.ErrorResponse"
|
||||||
"additionalProperties": {
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"500": {
|
"500": {
|
||||||
"description": "internal server error",
|
"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": {
|
"schema": {
|
||||||
"$ref": "#/definitions/dto.ErrorResponse"
|
"$ref": "#/definitions/dto.ErrorResponse"
|
||||||
}
|
}
|
||||||
@@ -515,9 +362,10 @@ const docTemplate = `{
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"post": {
|
"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": [
|
"consumes": [
|
||||||
"application/json"
|
"application/json",
|
||||||
|
"multipart/form-data"
|
||||||
],
|
],
|
||||||
"produces": [
|
"produces": [
|
||||||
"application/json"
|
"application/json"
|
||||||
@@ -528,10 +376,9 @@ const docTemplate = `{
|
|||||||
"summary": "Создать транзакцию",
|
"summary": "Создать транзакцию",
|
||||||
"parameters": [
|
"parameters": [
|
||||||
{
|
{
|
||||||
"description": "Transaction payload",
|
"description": "JSON payload for manual or receipt-based transaction creation",
|
||||||
"name": "transaction",
|
"name": "transaction",
|
||||||
"in": "body",
|
"in": "body",
|
||||||
"required": true,
|
|
||||||
"schema": {
|
"schema": {
|
||||||
"$ref": "#/definitions/dto.CreateTransactionRequest"
|
"$ref": "#/definitions/dto.CreateTransactionRequest"
|
||||||
}
|
}
|
||||||
@@ -816,13 +663,13 @@ const docTemplate = `{
|
|||||||
"400": {
|
"400": {
|
||||||
"description": "Bad Request",
|
"description": "Bad Request",
|
||||||
"schema": {
|
"schema": {
|
||||||
"$ref": "#/definitions/domain.UserErrorResponse"
|
"$ref": "#/definitions/dto.ErrorResponse"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"500": {
|
"500": {
|
||||||
"description": "Internal Server Error",
|
"description": "Internal Server Error",
|
||||||
"schema": {
|
"schema": {
|
||||||
"$ref": "#/definitions/domain.UserErrorResponse"
|
"$ref": "#/definitions/dto.ErrorResponse"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -860,19 +707,19 @@ const docTemplate = `{
|
|||||||
"400": {
|
"400": {
|
||||||
"description": "invalid telegram id",
|
"description": "invalid telegram id",
|
||||||
"schema": {
|
"schema": {
|
||||||
"$ref": "#/definitions/domain.UserErrorResponse"
|
"$ref": "#/definitions/dto.ErrorResponse"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"404": {
|
"404": {
|
||||||
"description": "user not found",
|
"description": "user not found",
|
||||||
"schema": {
|
"schema": {
|
||||||
"$ref": "#/definitions/domain.UserErrorResponse"
|
"$ref": "#/definitions/dto.ErrorResponse"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"500": {
|
"500": {
|
||||||
"description": "internal server error",
|
"description": "internal server error",
|
||||||
"schema": {
|
"schema": {
|
||||||
"$ref": "#/definitions/domain.UserErrorResponse"
|
"$ref": "#/definitions/dto.ErrorResponse"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -910,19 +757,19 @@ const docTemplate = `{
|
|||||||
"400": {
|
"400": {
|
||||||
"description": "invalid id",
|
"description": "invalid id",
|
||||||
"schema": {
|
"schema": {
|
||||||
"$ref": "#/definitions/domain.UserErrorResponse"
|
"$ref": "#/definitions/dto.ErrorResponse"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"404": {
|
"404": {
|
||||||
"description": "user not found",
|
"description": "user not found",
|
||||||
"schema": {
|
"schema": {
|
||||||
"$ref": "#/definitions/domain.UserErrorResponse"
|
"$ref": "#/definitions/dto.ErrorResponse"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"500": {
|
"500": {
|
||||||
"description": "internal server error",
|
"description": "internal server error",
|
||||||
"schema": {
|
"schema": {
|
||||||
"$ref": "#/definitions/domain.UserErrorResponse"
|
"$ref": "#/definitions/dto.ErrorResponse"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -958,19 +805,19 @@ const docTemplate = `{
|
|||||||
"400": {
|
"400": {
|
||||||
"description": "invalid id",
|
"description": "invalid id",
|
||||||
"schema": {
|
"schema": {
|
||||||
"$ref": "#/definitions/domain.UserErrorResponse"
|
"$ref": "#/definitions/dto.ErrorResponse"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"404": {
|
"404": {
|
||||||
"description": "user not found",
|
"description": "user not found",
|
||||||
"schema": {
|
"schema": {
|
||||||
"$ref": "#/definitions/domain.UserErrorResponse"
|
"$ref": "#/definitions/dto.ErrorResponse"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"500": {
|
"500": {
|
||||||
"description": "internal server error",
|
"description": "internal server error",
|
||||||
"schema": {
|
"schema": {
|
||||||
"$ref": "#/definitions/domain.UserErrorResponse"
|
"$ref": "#/definitions/dto.ErrorResponse"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1015,19 +862,19 @@ const docTemplate = `{
|
|||||||
"400": {
|
"400": {
|
||||||
"description": "invalid id or invalid body",
|
"description": "invalid id or invalid body",
|
||||||
"schema": {
|
"schema": {
|
||||||
"$ref": "#/definitions/domain.UserErrorResponse"
|
"$ref": "#/definitions/dto.ErrorResponse"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"404": {
|
"404": {
|
||||||
"description": "user not found",
|
"description": "user not found",
|
||||||
"schema": {
|
"schema": {
|
||||||
"$ref": "#/definitions/domain.UserErrorResponse"
|
"$ref": "#/definitions/dto.ErrorResponse"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"500": {
|
"500": {
|
||||||
"description": "internal server error",
|
"description": "internal server error",
|
||||||
"schema": {
|
"schema": {
|
||||||
"$ref": "#/definitions/domain.UserErrorResponse"
|
"$ref": "#/definitions/dto.ErrorResponse"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1035,55 +882,6 @@ const docTemplate = `{
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"definitions": {
|
"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": {
|
"domain.CreateFamilyRequest": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
@@ -1182,14 +980,6 @@ const docTemplate = `{
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"domain.UserErrorResponse": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"error": {
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"domain.UserResponse": {
|
"domain.UserResponse": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
@@ -1267,14 +1057,6 @@ const docTemplate = `{
|
|||||||
},
|
},
|
||||||
"dto.CreateTransactionRequest": {
|
"dto.CreateTransactionRequest": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"required": [
|
|
||||||
"amount",
|
|
||||||
"category",
|
|
||||||
"created_by",
|
|
||||||
"datetime",
|
|
||||||
"family_id",
|
|
||||||
"type"
|
|
||||||
],
|
|
||||||
"properties": {
|
"properties": {
|
||||||
"amount": {
|
"amount": {
|
||||||
"type": "number"
|
"type": "number"
|
||||||
@@ -1294,9 +1076,15 @@ const docTemplate = `{
|
|||||||
"family_id": {
|
"family_id": {
|
||||||
"type": "integer"
|
"type": "integer"
|
||||||
},
|
},
|
||||||
|
"receipt_date": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
"receipt_id": {
|
"receipt_id": {
|
||||||
"type": "integer"
|
"type": "integer"
|
||||||
},
|
},
|
||||||
|
"receipt_number": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
"type": {
|
"type": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -99,19 +99,13 @@
|
|||||||
"400": {
|
"400": {
|
||||||
"description": "invalid body",
|
"description": "invalid body",
|
||||||
"schema": {
|
"schema": {
|
||||||
"type": "object",
|
"$ref": "#/definitions/dto.ErrorResponse"
|
||||||
"additionalProperties": {
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"500": {
|
"500": {
|
||||||
"description": "internal server error",
|
"description": "internal server error",
|
||||||
"schema": {
|
"schema": {
|
||||||
"type": "object",
|
"$ref": "#/definitions/dto.ErrorResponse"
|
||||||
"additionalProperties": {
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -149,28 +143,19 @@
|
|||||||
"400": {
|
"400": {
|
||||||
"description": "invalid id",
|
"description": "invalid id",
|
||||||
"schema": {
|
"schema": {
|
||||||
"type": "object",
|
"$ref": "#/definitions/dto.ErrorResponse"
|
||||||
"additionalProperties": {
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"404": {
|
"404": {
|
||||||
"description": "family not found",
|
"description": "family not found",
|
||||||
"schema": {
|
"schema": {
|
||||||
"type": "object",
|
"$ref": "#/definitions/dto.ErrorResponse"
|
||||||
"additionalProperties": {
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"500": {
|
"500": {
|
||||||
"description": "internal server error",
|
"description": "internal server error",
|
||||||
"schema": {
|
"schema": {
|
||||||
"type": "object",
|
"$ref": "#/definitions/dto.ErrorResponse"
|
||||||
"additionalProperties": {
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -206,28 +191,19 @@
|
|||||||
"400": {
|
"400": {
|
||||||
"description": "invalid id",
|
"description": "invalid id",
|
||||||
"schema": {
|
"schema": {
|
||||||
"type": "object",
|
"$ref": "#/definitions/dto.ErrorResponse"
|
||||||
"additionalProperties": {
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"404": {
|
"404": {
|
||||||
"description": "family not found",
|
"description": "family not found",
|
||||||
"schema": {
|
"schema": {
|
||||||
"type": "object",
|
"$ref": "#/definitions/dto.ErrorResponse"
|
||||||
"additionalProperties": {
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"500": {
|
"500": {
|
||||||
"description": "internal server error",
|
"description": "internal server error",
|
||||||
"schema": {
|
"schema": {
|
||||||
"type": "object",
|
"$ref": "#/definitions/dto.ErrorResponse"
|
||||||
"additionalProperties": {
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -272,146 +248,17 @@
|
|||||||
"400": {
|
"400": {
|
||||||
"description": "name is required",
|
"description": "name is required",
|
||||||
"schema": {
|
"schema": {
|
||||||
"type": "object",
|
"$ref": "#/definitions/dto.ErrorResponse"
|
||||||
"additionalProperties": {
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"404": {
|
"404": {
|
||||||
"description": "family not found",
|
"description": "family not found",
|
||||||
"schema": {
|
"schema": {
|
||||||
"type": "object",
|
"$ref": "#/definitions/dto.ErrorResponse"
|
||||||
"additionalProperties": {
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"500": {
|
"500": {
|
||||||
"description": "internal server error",
|
"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": {
|
"schema": {
|
||||||
"$ref": "#/definitions/dto.ErrorResponse"
|
"$ref": "#/definitions/dto.ErrorResponse"
|
||||||
}
|
}
|
||||||
@@ -504,9 +351,10 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"post": {
|
"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": [
|
"consumes": [
|
||||||
"application/json"
|
"application/json",
|
||||||
|
"multipart/form-data"
|
||||||
],
|
],
|
||||||
"produces": [
|
"produces": [
|
||||||
"application/json"
|
"application/json"
|
||||||
@@ -517,10 +365,9 @@
|
|||||||
"summary": "Создать транзакцию",
|
"summary": "Создать транзакцию",
|
||||||
"parameters": [
|
"parameters": [
|
||||||
{
|
{
|
||||||
"description": "Transaction payload",
|
"description": "JSON payload for manual or receipt-based transaction creation",
|
||||||
"name": "transaction",
|
"name": "transaction",
|
||||||
"in": "body",
|
"in": "body",
|
||||||
"required": true,
|
|
||||||
"schema": {
|
"schema": {
|
||||||
"$ref": "#/definitions/dto.CreateTransactionRequest"
|
"$ref": "#/definitions/dto.CreateTransactionRequest"
|
||||||
}
|
}
|
||||||
@@ -805,13 +652,13 @@
|
|||||||
"400": {
|
"400": {
|
||||||
"description": "Bad Request",
|
"description": "Bad Request",
|
||||||
"schema": {
|
"schema": {
|
||||||
"$ref": "#/definitions/domain.UserErrorResponse"
|
"$ref": "#/definitions/dto.ErrorResponse"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"500": {
|
"500": {
|
||||||
"description": "Internal Server Error",
|
"description": "Internal Server Error",
|
||||||
"schema": {
|
"schema": {
|
||||||
"$ref": "#/definitions/domain.UserErrorResponse"
|
"$ref": "#/definitions/dto.ErrorResponse"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -849,19 +696,19 @@
|
|||||||
"400": {
|
"400": {
|
||||||
"description": "invalid telegram id",
|
"description": "invalid telegram id",
|
||||||
"schema": {
|
"schema": {
|
||||||
"$ref": "#/definitions/domain.UserErrorResponse"
|
"$ref": "#/definitions/dto.ErrorResponse"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"404": {
|
"404": {
|
||||||
"description": "user not found",
|
"description": "user not found",
|
||||||
"schema": {
|
"schema": {
|
||||||
"$ref": "#/definitions/domain.UserErrorResponse"
|
"$ref": "#/definitions/dto.ErrorResponse"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"500": {
|
"500": {
|
||||||
"description": "internal server error",
|
"description": "internal server error",
|
||||||
"schema": {
|
"schema": {
|
||||||
"$ref": "#/definitions/domain.UserErrorResponse"
|
"$ref": "#/definitions/dto.ErrorResponse"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -899,19 +746,19 @@
|
|||||||
"400": {
|
"400": {
|
||||||
"description": "invalid id",
|
"description": "invalid id",
|
||||||
"schema": {
|
"schema": {
|
||||||
"$ref": "#/definitions/domain.UserErrorResponse"
|
"$ref": "#/definitions/dto.ErrorResponse"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"404": {
|
"404": {
|
||||||
"description": "user not found",
|
"description": "user not found",
|
||||||
"schema": {
|
"schema": {
|
||||||
"$ref": "#/definitions/domain.UserErrorResponse"
|
"$ref": "#/definitions/dto.ErrorResponse"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"500": {
|
"500": {
|
||||||
"description": "internal server error",
|
"description": "internal server error",
|
||||||
"schema": {
|
"schema": {
|
||||||
"$ref": "#/definitions/domain.UserErrorResponse"
|
"$ref": "#/definitions/dto.ErrorResponse"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -947,19 +794,19 @@
|
|||||||
"400": {
|
"400": {
|
||||||
"description": "invalid id",
|
"description": "invalid id",
|
||||||
"schema": {
|
"schema": {
|
||||||
"$ref": "#/definitions/domain.UserErrorResponse"
|
"$ref": "#/definitions/dto.ErrorResponse"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"404": {
|
"404": {
|
||||||
"description": "user not found",
|
"description": "user not found",
|
||||||
"schema": {
|
"schema": {
|
||||||
"$ref": "#/definitions/domain.UserErrorResponse"
|
"$ref": "#/definitions/dto.ErrorResponse"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"500": {
|
"500": {
|
||||||
"description": "internal server error",
|
"description": "internal server error",
|
||||||
"schema": {
|
"schema": {
|
||||||
"$ref": "#/definitions/domain.UserErrorResponse"
|
"$ref": "#/definitions/dto.ErrorResponse"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1004,19 +851,19 @@
|
|||||||
"400": {
|
"400": {
|
||||||
"description": "invalid id or invalid body",
|
"description": "invalid id or invalid body",
|
||||||
"schema": {
|
"schema": {
|
||||||
"$ref": "#/definitions/domain.UserErrorResponse"
|
"$ref": "#/definitions/dto.ErrorResponse"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"404": {
|
"404": {
|
||||||
"description": "user not found",
|
"description": "user not found",
|
||||||
"schema": {
|
"schema": {
|
||||||
"$ref": "#/definitions/domain.UserErrorResponse"
|
"$ref": "#/definitions/dto.ErrorResponse"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"500": {
|
"500": {
|
||||||
"description": "internal server error",
|
"description": "internal server error",
|
||||||
"schema": {
|
"schema": {
|
||||||
"$ref": "#/definitions/domain.UserErrorResponse"
|
"$ref": "#/definitions/dto.ErrorResponse"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1024,55 +871,6 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"definitions": {
|
"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": {
|
"domain.CreateFamilyRequest": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
@@ -1171,14 +969,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"domain.UserErrorResponse": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"error": {
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"domain.UserResponse": {
|
"domain.UserResponse": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
@@ -1256,14 +1046,6 @@
|
|||||||
},
|
},
|
||||||
"dto.CreateTransactionRequest": {
|
"dto.CreateTransactionRequest": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"required": [
|
|
||||||
"amount",
|
|
||||||
"category",
|
|
||||||
"created_by",
|
|
||||||
"datetime",
|
|
||||||
"family_id",
|
|
||||||
"type"
|
|
||||||
],
|
|
||||||
"properties": {
|
"properties": {
|
||||||
"amount": {
|
"amount": {
|
||||||
"type": "number"
|
"type": "number"
|
||||||
@@ -1283,9 +1065,15 @@
|
|||||||
"family_id": {
|
"family_id": {
|
||||||
"type": "integer"
|
"type": "integer"
|
||||||
},
|
},
|
||||||
|
"receipt_date": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
"receipt_id": {
|
"receipt_id": {
|
||||||
"type": "integer"
|
"type": "integer"
|
||||||
},
|
},
|
||||||
|
"receipt_number": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
"type": {
|
"type": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,37 +1,4 @@
|
|||||||
definitions:
|
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:
|
domain.CreateFamilyRequest:
|
||||||
properties:
|
properties:
|
||||||
name:
|
name:
|
||||||
@@ -96,11 +63,6 @@ definitions:
|
|||||||
username:
|
username:
|
||||||
type: string
|
type: string
|
||||||
type: object
|
type: object
|
||||||
domain.UserErrorResponse:
|
|
||||||
properties:
|
|
||||||
error:
|
|
||||||
type: string
|
|
||||||
type: object
|
|
||||||
domain.UserResponse:
|
domain.UserResponse:
|
||||||
properties:
|
properties:
|
||||||
created_at:
|
created_at:
|
||||||
@@ -164,17 +126,14 @@ definitions:
|
|||||||
type: string
|
type: string
|
||||||
family_id:
|
family_id:
|
||||||
type: integer
|
type: integer
|
||||||
|
receipt_date:
|
||||||
|
type: string
|
||||||
receipt_id:
|
receipt_id:
|
||||||
type: integer
|
type: integer
|
||||||
|
receipt_number:
|
||||||
|
type: string
|
||||||
type:
|
type:
|
||||||
type: string
|
type: string
|
||||||
required:
|
|
||||||
- amount
|
|
||||||
- category
|
|
||||||
- created_by
|
|
||||||
- datetime
|
|
||||||
- family_id
|
|
||||||
- type
|
|
||||||
type: object
|
type: object
|
||||||
dto.ErrorResponse:
|
dto.ErrorResponse:
|
||||||
properties:
|
properties:
|
||||||
@@ -302,15 +261,11 @@ paths:
|
|||||||
"400":
|
"400":
|
||||||
description: invalid body
|
description: invalid body
|
||||||
schema:
|
schema:
|
||||||
additionalProperties:
|
$ref: '#/definitions/dto.ErrorResponse'
|
||||||
type: string
|
|
||||||
type: object
|
|
||||||
"500":
|
"500":
|
||||||
description: internal server error
|
description: internal server error
|
||||||
schema:
|
schema:
|
||||||
additionalProperties:
|
$ref: '#/definitions/dto.ErrorResponse'
|
||||||
type: string
|
|
||||||
type: object
|
|
||||||
summary: Создать семью
|
summary: Создать семью
|
||||||
tags:
|
tags:
|
||||||
- Families
|
- Families
|
||||||
@@ -335,21 +290,15 @@ paths:
|
|||||||
"400":
|
"400":
|
||||||
description: invalid id
|
description: invalid id
|
||||||
schema:
|
schema:
|
||||||
additionalProperties:
|
$ref: '#/definitions/dto.ErrorResponse'
|
||||||
type: string
|
|
||||||
type: object
|
|
||||||
"404":
|
"404":
|
||||||
description: family not found
|
description: family not found
|
||||||
schema:
|
schema:
|
||||||
additionalProperties:
|
$ref: '#/definitions/dto.ErrorResponse'
|
||||||
type: string
|
|
||||||
type: object
|
|
||||||
"500":
|
"500":
|
||||||
description: internal server error
|
description: internal server error
|
||||||
schema:
|
schema:
|
||||||
additionalProperties:
|
$ref: '#/definitions/dto.ErrorResponse'
|
||||||
type: string
|
|
||||||
type: object
|
|
||||||
summary: Удалить семью
|
summary: Удалить семью
|
||||||
tags:
|
tags:
|
||||||
- Families
|
- Families
|
||||||
@@ -373,21 +322,15 @@ paths:
|
|||||||
"400":
|
"400":
|
||||||
description: invalid id
|
description: invalid id
|
||||||
schema:
|
schema:
|
||||||
additionalProperties:
|
$ref: '#/definitions/dto.ErrorResponse'
|
||||||
type: string
|
|
||||||
type: object
|
|
||||||
"404":
|
"404":
|
||||||
description: family not found
|
description: family not found
|
||||||
schema:
|
schema:
|
||||||
additionalProperties:
|
$ref: '#/definitions/dto.ErrorResponse'
|
||||||
type: string
|
|
||||||
type: object
|
|
||||||
"500":
|
"500":
|
||||||
description: internal server error
|
description: internal server error
|
||||||
schema:
|
schema:
|
||||||
additionalProperties:
|
$ref: '#/definitions/dto.ErrorResponse'
|
||||||
type: string
|
|
||||||
type: object
|
|
||||||
summary: Получить семью по ID
|
summary: Получить семью по ID
|
||||||
tags:
|
tags:
|
||||||
- Families
|
- Families
|
||||||
@@ -417,105 +360,18 @@ paths:
|
|||||||
"400":
|
"400":
|
||||||
description: name is required
|
description: name is required
|
||||||
schema:
|
schema:
|
||||||
additionalProperties:
|
$ref: '#/definitions/dto.ErrorResponse'
|
||||||
type: string
|
|
||||||
type: object
|
|
||||||
"404":
|
"404":
|
||||||
description: family not found
|
description: family not found
|
||||||
schema:
|
schema:
|
||||||
additionalProperties:
|
$ref: '#/definitions/dto.ErrorResponse'
|
||||||
type: string
|
|
||||||
type: object
|
|
||||||
"500":
|
"500":
|
||||||
description: internal server error
|
description: internal server error
|
||||||
schema:
|
schema:
|
||||||
additionalProperties:
|
$ref: '#/definitions/dto.ErrorResponse'
|
||||||
type: string
|
|
||||||
type: object
|
|
||||||
summary: Обновить семью
|
summary: Обновить семью
|
||||||
tags:
|
tags:
|
||||||
- Families
|
- 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:
|
/api/v1/transactions:
|
||||||
get:
|
get:
|
||||||
consumes:
|
consumes:
|
||||||
@@ -575,13 +431,17 @@ paths:
|
|||||||
post:
|
post:
|
||||||
consumes:
|
consumes:
|
||||||
- application/json
|
- 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:
|
parameters:
|
||||||
- description: Transaction payload
|
- description: JSON payload for manual or receipt-based transaction creation
|
||||||
in: body
|
in: body
|
||||||
name: transaction
|
name: transaction
|
||||||
required: true
|
|
||||||
schema:
|
schema:
|
||||||
$ref: '#/definitions/dto.CreateTransactionRequest'
|
$ref: '#/definitions/dto.CreateTransactionRequest'
|
||||||
produces:
|
produces:
|
||||||
@@ -773,11 +633,11 @@ paths:
|
|||||||
"400":
|
"400":
|
||||||
description: Bad Request
|
description: Bad Request
|
||||||
schema:
|
schema:
|
||||||
$ref: '#/definitions/domain.UserErrorResponse'
|
$ref: '#/definitions/dto.ErrorResponse'
|
||||||
"500":
|
"500":
|
||||||
description: Internal Server Error
|
description: Internal Server Error
|
||||||
schema:
|
schema:
|
||||||
$ref: '#/definitions/domain.UserErrorResponse'
|
$ref: '#/definitions/dto.ErrorResponse'
|
||||||
summary: Создать пользователя
|
summary: Создать пользователя
|
||||||
tags:
|
tags:
|
||||||
- Users
|
- Users
|
||||||
@@ -802,15 +662,15 @@ paths:
|
|||||||
"400":
|
"400":
|
||||||
description: invalid id
|
description: invalid id
|
||||||
schema:
|
schema:
|
||||||
$ref: '#/definitions/domain.UserErrorResponse'
|
$ref: '#/definitions/dto.ErrorResponse'
|
||||||
"404":
|
"404":
|
||||||
description: user not found
|
description: user not found
|
||||||
schema:
|
schema:
|
||||||
$ref: '#/definitions/domain.UserErrorResponse'
|
$ref: '#/definitions/dto.ErrorResponse'
|
||||||
"500":
|
"500":
|
||||||
description: internal server error
|
description: internal server error
|
||||||
schema:
|
schema:
|
||||||
$ref: '#/definitions/domain.UserErrorResponse'
|
$ref: '#/definitions/dto.ErrorResponse'
|
||||||
summary: Удалить пользователя
|
summary: Удалить пользователя
|
||||||
tags:
|
tags:
|
||||||
- Users
|
- Users
|
||||||
@@ -834,15 +694,15 @@ paths:
|
|||||||
"400":
|
"400":
|
||||||
description: invalid id
|
description: invalid id
|
||||||
schema:
|
schema:
|
||||||
$ref: '#/definitions/domain.UserErrorResponse'
|
$ref: '#/definitions/dto.ErrorResponse'
|
||||||
"404":
|
"404":
|
||||||
description: user not found
|
description: user not found
|
||||||
schema:
|
schema:
|
||||||
$ref: '#/definitions/domain.UserErrorResponse'
|
$ref: '#/definitions/dto.ErrorResponse'
|
||||||
"500":
|
"500":
|
||||||
description: internal server error
|
description: internal server error
|
||||||
schema:
|
schema:
|
||||||
$ref: '#/definitions/domain.UserErrorResponse'
|
$ref: '#/definitions/dto.ErrorResponse'
|
||||||
summary: Получить пользователя по ID
|
summary: Получить пользователя по ID
|
||||||
tags:
|
tags:
|
||||||
- Users
|
- Users
|
||||||
@@ -872,15 +732,15 @@ paths:
|
|||||||
"400":
|
"400":
|
||||||
description: invalid id or invalid body
|
description: invalid id or invalid body
|
||||||
schema:
|
schema:
|
||||||
$ref: '#/definitions/domain.UserErrorResponse'
|
$ref: '#/definitions/dto.ErrorResponse'
|
||||||
"404":
|
"404":
|
||||||
description: user not found
|
description: user not found
|
||||||
schema:
|
schema:
|
||||||
$ref: '#/definitions/domain.UserErrorResponse'
|
$ref: '#/definitions/dto.ErrorResponse'
|
||||||
"500":
|
"500":
|
||||||
description: internal server error
|
description: internal server error
|
||||||
schema:
|
schema:
|
||||||
$ref: '#/definitions/domain.UserErrorResponse'
|
$ref: '#/definitions/dto.ErrorResponse'
|
||||||
summary: Обновить пользователя
|
summary: Обновить пользователя
|
||||||
tags:
|
tags:
|
||||||
- Users
|
- Users
|
||||||
@@ -905,15 +765,15 @@ paths:
|
|||||||
"400":
|
"400":
|
||||||
description: invalid telegram id
|
description: invalid telegram id
|
||||||
schema:
|
schema:
|
||||||
$ref: '#/definitions/domain.UserErrorResponse'
|
$ref: '#/definitions/dto.ErrorResponse'
|
||||||
"404":
|
"404":
|
||||||
description: user not found
|
description: user not found
|
||||||
schema:
|
schema:
|
||||||
$ref: '#/definitions/domain.UserErrorResponse'
|
$ref: '#/definitions/dto.ErrorResponse'
|
||||||
"500":
|
"500":
|
||||||
description: internal server error
|
description: internal server error
|
||||||
schema:
|
schema:
|
||||||
$ref: '#/definitions/domain.UserErrorResponse'
|
$ref: '#/definitions/dto.ErrorResponse'
|
||||||
summary: Получить пользователя по Telegram ID
|
summary: Получить пользователя по Telegram ID
|
||||||
tags:
|
tags:
|
||||||
- Users
|
- 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/domain"
|
||||||
"FamilyHub/src/utils"
|
"FamilyHub/src/utils"
|
||||||
"errors"
|
"errors"
|
||||||
"strconv"
|
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"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) {
|
func BuildCreateTransactionInput(req dto.CreateTransactionRequest) (services.CreateTransactionInput, error) {
|
||||||
if req.ReceiptNumber != nil || req.ReceiptDate != nil {
|
if req.ReceiptNumber != nil || req.ReceiptDate != nil {
|
||||||
receiptReq, err := BuildReceiptTransactionRequest(req)
|
receiptReq, err := BuildReceiptTransactionRequest(req)
|
||||||
@@ -30,24 +36,15 @@ func BuildCreateTransactionInput(req dto.CreateTransactionRequest) (services.Cre
|
|||||||
return services.CreateTransactionInput{Manual: &manualReq}, nil
|
return services.CreateTransactionInput{Manual: &manualReq}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func BuildPhotoCreateTransactionInput(c *gin.Context, image []byte) (services.CreateTransactionInput, error) {
|
func BuildPhotoCreateTransactionInput(fields PhotoCreateTransactionFields) (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
|
|
||||||
}
|
|
||||||
|
|
||||||
return services.CreateTransactionInput{
|
return services.CreateTransactionInput{
|
||||||
Photo: &services.CreateTransactionPhotoInput{
|
Photo: &services.CreateTransactionPhotoInput{
|
||||||
Image: image,
|
Image: fields.Image,
|
||||||
FamilyID: familyID,
|
FamilyID: fields.FamilyID,
|
||||||
CreatedBy: createdBy,
|
CreatedBy: fields.CreatedBy,
|
||||||
Type: parseOptionalStringForm(c, "type"),
|
Type: trimOptionalString(fields.Type),
|
||||||
Category: parseOptionalStringForm(c, "category"),
|
Category: trimOptionalString(fields.Category),
|
||||||
Description: parseOptionalStringForm(c, "description"),
|
Description: trimOptionalString(fields.Description),
|
||||||
},
|
},
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
@@ -128,26 +125,3 @@ func trimOptionalString(value *string) *string {
|
|||||||
|
|
||||||
return &trimmed
|
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 (
|
import (
|
||||||
"FamilyHub/src/api/dto"
|
"FamilyHub/src/api/dto"
|
||||||
|
"FamilyHub/src/api/requests"
|
||||||
"FamilyHub/src/api/services"
|
"FamilyHub/src/api/services"
|
||||||
"FamilyHub/src/domain"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
@@ -45,15 +45,10 @@ func (router *ActivitiesRouter) List(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
activities, filter, err := router.service.List(c.Request.Context(), domain.ActivityLogListFilter{
|
filter := requests.BuildActivityListFilter(query)
|
||||||
FamilyID: query.FamilyID,
|
activities, filter, err := router.service.List(c.Request.Context(), filter)
|
||||||
UserID: query.UserID,
|
|
||||||
Limit: query.Limit,
|
|
||||||
Offset: query.Offset,
|
|
||||||
})
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logInternalError(c, "activity request", err)
|
handleActivityError(c, err)
|
||||||
c.JSON(http.StatusInternalServerError, dto.ErrorResponse{Message: "internal server error"})
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,10 @@ package routers
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"FamilyHub/src/api/dto"
|
"FamilyHub/src/api/dto"
|
||||||
|
"FamilyHub/src/api/requests"
|
||||||
|
"FamilyHub/src/api/services"
|
||||||
receiptServiceIntegration "FamilyHub/src/integrations/receiptProvider"
|
receiptServiceIntegration "FamilyHub/src/integrations/receiptProvider"
|
||||||
|
"database/sql"
|
||||||
"errors"
|
"errors"
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
@@ -60,3 +63,36 @@ func handleReceiptError(c *gin.Context, err error) {
|
|||||||
c.JSON(http.StatusInternalServerError, dto.ErrorResponse{Message: "internal server 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
|
package routers
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"FamilyHub/src/api/dto"
|
||||||
|
"FamilyHub/src/api/requests"
|
||||||
"FamilyHub/src/api/services"
|
"FamilyHub/src/api/services"
|
||||||
"FamilyHub/src/domain"
|
"FamilyHub/src/domain"
|
||||||
"database/sql"
|
|
||||||
"errors"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
)
|
)
|
||||||
@@ -37,15 +36,15 @@ func (router *FamiliesRouter) RegisterRoutes(r *gin.RouterGroup) {
|
|||||||
// @Produce json
|
// @Produce json
|
||||||
// @Param family body domain.CreateFamilyRequest true "Family info"
|
// @Param family body domain.CreateFamilyRequest true "Family info"
|
||||||
// @Success 201 {object} domain.FamilyResponse
|
// @Success 201 {object} domain.FamilyResponse
|
||||||
// @Failure 400 {object} map[string]string "invalid body"
|
// @Failure 400 {object} dto.ErrorResponse "invalid body"
|
||||||
// @Failure 500 {object} map[string]string "internal server error"
|
// @Failure 500 {object} dto.ErrorResponse "internal server error"
|
||||||
// @Router /api/v1/families [post]
|
// @Router /api/v1/families [post]
|
||||||
func (router *FamiliesRouter) Create(c *gin.Context) {
|
func (router *FamiliesRouter) Create(c *gin.Context) {
|
||||||
var req domain.CreateFamilyRequest
|
var req domain.CreateFamilyRequest
|
||||||
var resp domain.FamilyResponse
|
var resp domain.FamilyResponse
|
||||||
|
|
||||||
if err := c.ShouldBindJSON(&req); err != nil {
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -66,16 +65,16 @@ func (router *FamiliesRouter) Create(c *gin.Context) {
|
|||||||
// @Produce json
|
// @Produce json
|
||||||
// @Param id path int true "Family ID"
|
// @Param id path int true "Family ID"
|
||||||
// @Success 200 {object} domain.FamilyResponse
|
// @Success 200 {object} domain.FamilyResponse
|
||||||
// @Failure 400 {object} map[string]string "invalid id"
|
// @Failure 400 {object} dto.ErrorResponse "invalid id"
|
||||||
// @Failure 404 {object} map[string]string "family not found"
|
// @Failure 404 {object} dto.ErrorResponse "family not found"
|
||||||
// @Failure 500 {object} map[string]string "internal server error"
|
// @Failure 500 {object} dto.ErrorResponse "internal server error"
|
||||||
// @Router /api/v1/families/{id} [get]
|
// @Router /api/v1/families/{id} [get]
|
||||||
func (router *FamiliesRouter) Read(c *gin.Context) {
|
func (router *FamiliesRouter) Read(c *gin.Context) {
|
||||||
var resp domain.FamilyResponse
|
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 {
|
if err != nil {
|
||||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid id"})
|
c.JSON(http.StatusBadRequest, dto.ErrorResponse{Message: err.Error()})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -97,27 +96,27 @@ func (router *FamiliesRouter) Read(c *gin.Context) {
|
|||||||
// @Param id path int true "Family ID"
|
// @Param id path int true "Family ID"
|
||||||
// @Param family body domain.UpdateFamilyRequest true "Данные для обновления"
|
// @Param family body domain.UpdateFamilyRequest true "Данные для обновления"
|
||||||
// @Success 200 {object} domain.FamilyResponse
|
// @Success 200 {object} domain.FamilyResponse
|
||||||
// @Failure 400 {object} map[string]string "invalid id or invalid body"
|
// @Failure 400 {object} dto.ErrorResponse "invalid id or invalid body"
|
||||||
// @Failure 400 {object} map[string]string "name is required"
|
// @Failure 400 {object} dto.ErrorResponse "name is required"
|
||||||
// @Failure 404 {object} map[string]string "family not found"
|
// @Failure 404 {object} dto.ErrorResponse "family not found"
|
||||||
// @Failure 500 {object} map[string]string "internal server error"
|
// @Failure 500 {object} dto.ErrorResponse "internal server error"
|
||||||
// @Router /api/v1/families/{id} [patch]
|
// @Router /api/v1/families/{id} [patch]
|
||||||
func (router *FamiliesRouter) Update(c *gin.Context) {
|
func (router *FamiliesRouter) Update(c *gin.Context) {
|
||||||
var resp domain.FamilyResponse
|
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 {
|
if err != nil {
|
||||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid id"})
|
c.JSON(http.StatusBadRequest, dto.ErrorResponse{Message: err.Error()})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var req domain.UpdateFamilyRequest
|
var req domain.UpdateFamilyRequest
|
||||||
if err := c.ShouldBindJSON(&req); err != nil {
|
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
|
return
|
||||||
}
|
}
|
||||||
if req.Name == nil {
|
if err := requests.ValidateFamilyUpdate(req); err != nil {
|
||||||
c.JSON(http.StatusBadRequest, gin.H{"error": "name is required"})
|
c.JSON(http.StatusBadRequest, dto.ErrorResponse{Message: err.Error()})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -138,14 +137,14 @@ func (router *FamiliesRouter) Update(c *gin.Context) {
|
|||||||
// @Produce json
|
// @Produce json
|
||||||
// @Param id path int true "Family ID"
|
// @Param id path int true "Family ID"
|
||||||
// @Success 204 {string} string "no content"
|
// @Success 204 {string} string "no content"
|
||||||
// @Failure 400 {object} map[string]string "invalid id"
|
// @Failure 400 {object} dto.ErrorResponse "invalid id"
|
||||||
// @Failure 404 {object} map[string]string "family not found"
|
// @Failure 404 {object} dto.ErrorResponse "family not found"
|
||||||
// @Failure 500 {object} map[string]string "internal server error"
|
// @Failure 500 {object} dto.ErrorResponse "internal server error"
|
||||||
// @Router /api/v1/families/{id} [delete]
|
// @Router /api/v1/families/{id} [delete]
|
||||||
func (router *FamiliesRouter) Delete(c *gin.Context) {
|
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 {
|
if err != nil {
|
||||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid id"})
|
c.JSON(http.StatusBadRequest, dto.ErrorResponse{Message: err.Error()})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -156,15 +155,3 @@ func (router *FamiliesRouter) Delete(c *gin.Context) {
|
|||||||
|
|
||||||
c.Status(http.StatusNoContent)
|
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)
|
r.ServeHTTP(w, req)
|
||||||
|
|
||||||
require.Equal(t, http.StatusBadRequest, w.Code)
|
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) {
|
t.Run("internal error", func(t *testing.T) {
|
||||||
@@ -205,7 +205,7 @@ func TestFamiliesRouter_Update(t *testing.T) {
|
|||||||
r.ServeHTTP(w, req)
|
r.ServeHTTP(w, req)
|
||||||
|
|
||||||
require.Equal(t, http.StatusBadRequest, w.Code)
|
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) {
|
t.Run("bad request on missing name", func(t *testing.T) {
|
||||||
|
|||||||
@@ -41,12 +41,16 @@ func (router *TransactionsRouter) RegisterRoutes(r *gin.RouterGroup) {
|
|||||||
|
|
||||||
// Create GoDoc
|
// Create GoDoc
|
||||||
// @Summary Создать транзакцию
|
// @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
|
// @Tags Transactions
|
||||||
// @Accept json
|
// @Accept json
|
||||||
// @Accept multipart/form-data
|
// @Accept multipart/form-data
|
||||||
// @Produce json
|
// @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
|
// @Success 201 {object} dto.TransactionResponse
|
||||||
// @Failure 400 {object} dto.ErrorResponse
|
// @Failure 400 {object} dto.ErrorResponse
|
||||||
// @Failure 404 {object} dto.ErrorResponse
|
// @Failure 404 {object} dto.ErrorResponse
|
||||||
@@ -101,7 +105,25 @@ func (router *TransactionsRouter) createFromMultipart(c *gin.Context) {
|
|||||||
return
|
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 {
|
if err != nil {
|
||||||
c.JSON(http.StatusBadRequest, dto.ErrorResponse{Message: err.Error()})
|
c.JSON(http.StatusBadRequest, dto.ErrorResponse{Message: err.Error()})
|
||||||
return
|
return
|
||||||
@@ -364,3 +386,26 @@ func transactionQueryToFilter(query dto.ListTransactionsQuery) (domain.Transacti
|
|||||||
|
|
||||||
return filter, nil
|
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
|
package routers
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"FamilyHub/src/api/dto"
|
||||||
|
"FamilyHub/src/api/requests"
|
||||||
"FamilyHub/src/api/services"
|
"FamilyHub/src/api/services"
|
||||||
"FamilyHub/src/domain"
|
"FamilyHub/src/domain"
|
||||||
"errors"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
)
|
)
|
||||||
@@ -36,21 +36,21 @@ func (router *UsersRouter) RegisterRoutes(r *gin.RouterGroup) {
|
|||||||
// @Produce json
|
// @Produce json
|
||||||
// @Param user body domain.CreateUserRequest true "User info"
|
// @Param user body domain.CreateUserRequest true "User info"
|
||||||
// @Success 201 {object} domain.UserResponse
|
// @Success 201 {object} domain.UserResponse
|
||||||
// @Failure 400 {object} domain.UserErrorResponse
|
// @Failure 400 {object} dto.ErrorResponse
|
||||||
// @Failure 500 {object} domain.UserErrorResponse
|
// @Failure 500 {object} dto.ErrorResponse
|
||||||
// @Router /api/v1/users [post]
|
// @Router /api/v1/users [post]
|
||||||
func (router *UsersRouter) Create(c *gin.Context) {
|
func (router *UsersRouter) Create(c *gin.Context) {
|
||||||
var req domain.CreateUserRequest
|
var req domain.CreateUserRequest
|
||||||
var resp domain.UserResponse
|
var resp domain.UserResponse
|
||||||
|
|
||||||
if err := c.ShouldBindJSON(&req); err != nil {
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
user, err := router.service.Create(c.Request.Context(), req)
|
user, err := router.service.Create(c.Request.Context(), req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
handleError(c, err)
|
handleUserError(c, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -65,21 +65,21 @@ func (router *UsersRouter) Create(c *gin.Context) {
|
|||||||
// @Produce json
|
// @Produce json
|
||||||
// @Param id path int true "User ID"
|
// @Param id path int true "User ID"
|
||||||
// @Success 200 {object} domain.UserResponse
|
// @Success 200 {object} domain.UserResponse
|
||||||
// @Failure 400 {object} domain.UserErrorResponse "invalid id"
|
// @Failure 400 {object} dto.ErrorResponse "invalid id"
|
||||||
// @Failure 404 {object} domain.UserErrorResponse "user not found"
|
// @Failure 404 {object} dto.ErrorResponse "user not found"
|
||||||
// @Failure 500 {object} domain.UserErrorResponse "internal server error"
|
// @Failure 500 {object} dto.ErrorResponse "internal server error"
|
||||||
// @Router /api/v1/users/{id} [get]
|
// @Router /api/v1/users/{id} [get]
|
||||||
func (router *UsersRouter) Read(c *gin.Context) {
|
func (router *UsersRouter) Read(c *gin.Context) {
|
||||||
var resp domain.UserResponse
|
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 {
|
if err != nil {
|
||||||
c.JSON(http.StatusBadRequest, domain.UserErrorResponse{Error: "invalid id"})
|
c.JSON(http.StatusBadRequest, dto.ErrorResponse{Message: err.Error()})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
user, err := router.service.GetByID(c.Request.Context(), id)
|
user, err := router.service.GetByID(c.Request.Context(), id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
handleError(c, err)
|
handleUserError(c, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -94,21 +94,21 @@ func (router *UsersRouter) Read(c *gin.Context) {
|
|||||||
// @Produce json
|
// @Produce json
|
||||||
// @Param telegramId path int true "Telegram ID"
|
// @Param telegramId path int true "Telegram ID"
|
||||||
// @Success 200 {object} domain.UserResponse
|
// @Success 200 {object} domain.UserResponse
|
||||||
// @Failure 400 {object} domain.UserErrorResponse "invalid telegram id"
|
// @Failure 400 {object} dto.ErrorResponse "invalid telegram id"
|
||||||
// @Failure 404 {object} domain.UserErrorResponse "user not found"
|
// @Failure 404 {object} dto.ErrorResponse "user not found"
|
||||||
// @Failure 500 {object} domain.UserErrorResponse "internal server error"
|
// @Failure 500 {object} dto.ErrorResponse "internal server error"
|
||||||
// @Router /api/v1/users/by-telegram/{telegramId} [get]
|
// @Router /api/v1/users/by-telegram/{telegramId} [get]
|
||||||
func (router *UsersRouter) GetByTelegramID(c *gin.Context) {
|
func (router *UsersRouter) GetByTelegramID(c *gin.Context) {
|
||||||
var resp domain.UserResponse
|
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 {
|
if err != nil {
|
||||||
c.JSON(http.StatusBadRequest, domain.UserErrorResponse{Error: "invalid telegram id"})
|
c.JSON(http.StatusBadRequest, dto.ErrorResponse{Message: err.Error()})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
user, err := router.service.GetByTelegramID(c.Request.Context(), telegramID)
|
user, err := router.service.GetByTelegramID(c.Request.Context(), telegramID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
handleError(c, err)
|
handleUserError(c, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -124,27 +124,27 @@ func (router *UsersRouter) GetByTelegramID(c *gin.Context) {
|
|||||||
// @Param id path int true "User ID"
|
// @Param id path int true "User ID"
|
||||||
// @Param user body domain.UpdateUserRequest true "Данные для обновления"
|
// @Param user body domain.UpdateUserRequest true "Данные для обновления"
|
||||||
// @Success 200 {object} domain.UserResponse
|
// @Success 200 {object} domain.UserResponse
|
||||||
// @Failure 400 {object} domain.UserErrorResponse "invalid id or invalid body"
|
// @Failure 400 {object} dto.ErrorResponse "invalid id or invalid body"
|
||||||
// @Failure 404 {object} domain.UserErrorResponse "user not found"
|
// @Failure 404 {object} dto.ErrorResponse "user not found"
|
||||||
// @Failure 500 {object} domain.UserErrorResponse "internal server error"
|
// @Failure 500 {object} dto.ErrorResponse "internal server error"
|
||||||
// @Router /api/v1/users/{id} [patch]
|
// @Router /api/v1/users/{id} [patch]
|
||||||
func (router *UsersRouter) Update(c *gin.Context) {
|
func (router *UsersRouter) Update(c *gin.Context) {
|
||||||
var resp domain.UserResponse
|
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 {
|
if err != nil {
|
||||||
c.JSON(http.StatusBadRequest, domain.UserErrorResponse{Error: "invalid id"})
|
c.JSON(http.StatusBadRequest, dto.ErrorResponse{Message: err.Error()})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var req domain.UpdateUserRequest
|
var req domain.UpdateUserRequest
|
||||||
if err := c.ShouldBindJSON(&req); err != nil {
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
user, err := router.service.Update(c.Request.Context(), id, req)
|
user, err := router.service.Update(c.Request.Context(), id, req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
handleError(c, err)
|
handleUserError(c, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -159,35 +159,21 @@ func (router *UsersRouter) Update(c *gin.Context) {
|
|||||||
// @Produce json
|
// @Produce json
|
||||||
// @Param id path int true "User ID"
|
// @Param id path int true "User ID"
|
||||||
// @Success 204 {string} string "no content"
|
// @Success 204 {string} string "no content"
|
||||||
// @Failure 400 {object} domain.UserErrorResponse "invalid id"
|
// @Failure 400 {object} dto.ErrorResponse "invalid id"
|
||||||
// @Failure 404 {object} domain.UserErrorResponse "user not found"
|
// @Failure 404 {object} dto.ErrorResponse "user not found"
|
||||||
// @Failure 500 {object} domain.UserErrorResponse "internal server error"
|
// @Failure 500 {object} dto.ErrorResponse "internal server error"
|
||||||
// @Router /api/v1/users/{id} [delete]
|
// @Router /api/v1/users/{id} [delete]
|
||||||
func (router *UsersRouter) Delete(c *gin.Context) {
|
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 {
|
if err != nil {
|
||||||
c.JSON(http.StatusBadRequest, domain.UserErrorResponse{Error: "invalid id"})
|
c.JSON(http.StatusBadRequest, dto.ErrorResponse{Message: err.Error()})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := router.service.Delete(c.Request.Context(), id); err != nil {
|
if err := router.service.Delete(c.Request.Context(), id); err != nil {
|
||||||
handleError(c, err)
|
handleUserError(c, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
c.Status(http.StatusNoContent)
|
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)
|
r.ServeHTTP(w, req)
|
||||||
|
|
||||||
require.Equal(t, http.StatusBadRequest, w.Code)
|
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) {
|
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)
|
r.ServeHTTP(w, req)
|
||||||
|
|
||||||
require.Equal(t, http.StatusBadRequest, w.Code)
|
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) {
|
t.Run("bad request on invalid patch", func(t *testing.T) {
|
||||||
|
|||||||
@@ -41,10 +41,6 @@ type UserResponse struct {
|
|||||||
UpdatedAt string `json:"updated_at"`
|
UpdatedAt string `json:"updated_at"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type UserErrorResponse struct {
|
|
||||||
Error string `json:"error"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (response *UserResponse) ModelToResponse(u *UserModel) UserResponse {
|
func (response *UserResponse) ModelToResponse(u *UserModel) UserResponse {
|
||||||
return UserResponse{
|
return UserResponse{
|
||||||
ID: u.ID,
|
ID: u.ID,
|
||||||
|
|||||||
@@ -7,9 +7,10 @@
|
|||||||
- расходов
|
- расходов
|
||||||
- категорий
|
- категорий
|
||||||
|
|
||||||
Поддерживает два способа ввода расходов:
|
Поддерживает три способа ввода расходов:
|
||||||
1. Ручной ввод
|
1. Ручной ввод
|
||||||
2. Сканирование чека (QR-код)
|
2. Ввод номера и даты чека
|
||||||
|
3. Загрузка фото чека
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -155,13 +156,27 @@ categories (
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### 4.3 Сканирование чека
|
### 4.3 Добавление расхода по номеру и дате чека
|
||||||
|
|
||||||
#### Поток:
|
#### Поток:
|
||||||
1. Пользователь отправляет QR-код
|
1. Пользователь вводит номер и дату чека
|
||||||
2. Backend получает данные чека через внешний сервис
|
2. Backend получает данные чека через внешний сервис
|
||||||
3. Создаётся запись в `receipts`
|
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 (
|
|||||||
|
|
||||||
### Ручной ввод
|
### Ручной ввод
|
||||||
|
|
||||||
```
|
```text
|
||||||
User → API → positions
|
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 (черновик)
|
## 6. API (черновик)
|
||||||
|
|
||||||
### Создание позиции
|
### Создание транзакции вручную
|
||||||
|
|
||||||
```
|
```http
|
||||||
POST /positions
|
POST /api/v1/transactions
|
||||||
|
Content-Type: application/json
|
||||||
```
|
```
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
|
"family_id": 1,
|
||||||
|
"created_by": 2,
|
||||||
|
"type": "expense",
|
||||||
|
"category": "groceries",
|
||||||
"amount": 1000,
|
"amount": 1000,
|
||||||
"category_id": 1,
|
"description": "Продукты",
|
||||||
"description": "Продукты"
|
"datetime": "2026-01-21T10:11:12Z"
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### Сканирование чека
|
### Создание транзакции по номеру и дате чека
|
||||||
|
|
||||||
```
|
```http
|
||||||
POST /receipts/scan
|
POST /api/v1/transactions
|
||||||
|
Content-Type: application/json
|
||||||
```
|
```
|
||||||
|
|
||||||
```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. Задачи для разработки
|
## 7. Задачи для разработки
|
||||||
|
|
||||||
### Этап 1 — База
|
### Этап 1 — База
|
||||||
@@ -242,10 +310,10 @@ GET /positions
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### Этап 3 — Позиции
|
### Этап 3 — Транзакции
|
||||||
|
|
||||||
- [ ] Endpoint создания позиции
|
- [x] Endpoint создания транзакции
|
||||||
- [ ] Endpoint получения списка
|
- [x] Endpoint получения списка
|
||||||
- [ ] Фильтрация
|
- [ ] Фильтрация
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -259,9 +327,11 @@ GET /positions
|
|||||||
|
|
||||||
### Этап 5 — Чеки
|
### Этап 5 — Чеки
|
||||||
|
|
||||||
- [ ] Endpoint загрузки QR
|
- [x] Endpoint загрузки фото чека через `POST /transactions`
|
||||||
- [ ] Интеграция с сервисом чеков
|
- [ ] Интеграция с сервисом чеков
|
||||||
- [ ] Создание receipts
|
- [x] Создание receipts
|
||||||
|
- [x] Создание транзакции по номеру и дате чека
|
||||||
|
- [x] Создание транзакции по фото чека
|
||||||
- [ ] Создание positions
|
- [ ] Создание positions
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -295,5 +365,5 @@ GET /positions
|
|||||||
- [ ] Нужна ли мультивалютность?
|
- [ ] Нужна ли мультивалютность?
|
||||||
- [ ] Можно ли редактировать чек?
|
- [ ] Можно ли редактировать чек?
|
||||||
- [ ] Как обрабатывать ошибки OCR?
|
- [ ] Как обрабатывать ошибки OCR?
|
||||||
|
- [ ] Нужен ли отдельный endpoint для повторной привязки чека к существующей транзакции?
|
||||||
- [ ] Нужны ли роли внутри семьи?
|
- [ ] Нужны ли роли внутри семьи?
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user