Added transaction feature, fixed some mistakes
This commit is contained in:
@@ -249,6 +249,342 @@ const docTemplate = `{
|
||||
}
|
||||
}
|
||||
},
|
||||
"/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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/transactions": {
|
||||
"get": {
|
||||
"description": "Возвращает список транзакций с фильтрами и пагинацией",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"Transactions"
|
||||
],
|
||||
"summary": "Получить список транзакций",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "integer",
|
||||
"description": "Family ID",
|
||||
"name": "family_id",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"description": "User ID",
|
||||
"name": "created_by",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Transaction type",
|
||||
"name": "type",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Category",
|
||||
"name": "category",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "RFC3339 start datetime",
|
||||
"name": "date_from",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "RFC3339 end datetime",
|
||||
"name": "date_to",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"description": "Limit, default 50",
|
||||
"name": "limit",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"description": "Offset",
|
||||
"name": "offset",
|
||||
"in": "query"
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/dto.TransactionListResponse"
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Bad Request",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/dto.ErrorResponse"
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "Internal Server Error",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/dto.ErrorResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"post": {
|
||||
"description": "Создает новую транзакцию и при необходимости привязывает к ней чек",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"Transactions"
|
||||
],
|
||||
"summary": "Создать транзакцию",
|
||||
"parameters": [
|
||||
{
|
||||
"description": "Transaction payload",
|
||||
"name": "transaction",
|
||||
"in": "body",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/dto.CreateTransactionRequest"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"201": {
|
||||
"description": "Created",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/dto.TransactionResponse"
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Bad Request",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/dto.ErrorResponse"
|
||||
}
|
||||
},
|
||||
"404": {
|
||||
"description": "Not Found",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/dto.ErrorResponse"
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "Internal Server Error",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/dto.ErrorResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/transactions/{id}": {
|
||||
"get": {
|
||||
"description": "Возвращает транзакцию по ее внутреннему ID",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"Transactions"
|
||||
],
|
||||
"summary": "Получить транзакцию по ID",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "integer",
|
||||
"description": "Transaction ID",
|
||||
"name": "id",
|
||||
"in": "path",
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/dto.TransactionResponse"
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Bad Request",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/dto.ErrorResponse"
|
||||
}
|
||||
},
|
||||
"404": {
|
||||
"description": "Not Found",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/dto.ErrorResponse"
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "Internal Server Error",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/dto.ErrorResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"delete": {
|
||||
"description": "Удаляет транзакцию по ID",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"Transactions"
|
||||
],
|
||||
"summary": "Удалить транзакцию",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "integer",
|
||||
"description": "Transaction ID",
|
||||
"name": "id",
|
||||
"in": "path",
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"204": {
|
||||
"description": "no content",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Bad Request",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/dto.ErrorResponse"
|
||||
}
|
||||
},
|
||||
"404": {
|
||||
"description": "Not Found",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/dto.ErrorResponse"
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "Internal Server Error",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/dto.ErrorResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"patch": {
|
||||
"description": "Частично обновляет данные транзакции и связь с чеком",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"Transactions"
|
||||
],
|
||||
"summary": "Обновить транзакцию",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "integer",
|
||||
"description": "Transaction ID",
|
||||
"name": "id",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"description": "Transaction patch payload",
|
||||
"name": "transaction",
|
||||
"in": "body",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/dto.UpdateTransactionRequest"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/dto.TransactionResponse"
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Bad Request",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/dto.ErrorResponse"
|
||||
}
|
||||
},
|
||||
"404": {
|
||||
"description": "Not Found",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/dto.ErrorResponse"
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "Internal Server Error",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/dto.ErrorResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/users": {
|
||||
"post": {
|
||||
"consumes": [
|
||||
@@ -501,6 +837,55 @@ 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": {
|
||||
@@ -574,6 +959,9 @@ const docTemplate = `{
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"telegram_chat_id": {
|
||||
"type": "integer"
|
||||
},
|
||||
"telegram_chat_name": {
|
||||
"type": "string"
|
||||
}
|
||||
@@ -632,6 +1020,123 @@ const docTemplate = `{
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"dto.CreateTransactionRequest": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"amount",
|
||||
"category",
|
||||
"created_by",
|
||||
"datetime",
|
||||
"family_id",
|
||||
"type"
|
||||
],
|
||||
"properties": {
|
||||
"amount": {
|
||||
"type": "number"
|
||||
},
|
||||
"category": {
|
||||
"type": "string"
|
||||
},
|
||||
"created_by": {
|
||||
"type": "integer"
|
||||
},
|
||||
"datetime": {
|
||||
"type": "string"
|
||||
},
|
||||
"description": {
|
||||
"type": "string"
|
||||
},
|
||||
"family_id": {
|
||||
"type": "integer"
|
||||
},
|
||||
"receipt_id": {
|
||||
"type": "integer"
|
||||
},
|
||||
"type": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"dto.ErrorResponse": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"message": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"dto.TransactionListResponse": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"items": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/dto.TransactionResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"dto.TransactionResponse": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"amount": {
|
||||
"type": "number"
|
||||
},
|
||||
"category": {
|
||||
"type": "string"
|
||||
},
|
||||
"created_at": {
|
||||
"type": "string"
|
||||
},
|
||||
"created_by": {
|
||||
"type": "integer"
|
||||
},
|
||||
"datetime": {
|
||||
"type": "string"
|
||||
},
|
||||
"description": {
|
||||
"type": "string"
|
||||
},
|
||||
"family_id": {
|
||||
"type": "integer"
|
||||
},
|
||||
"id": {
|
||||
"type": "integer"
|
||||
},
|
||||
"receipt_id": {
|
||||
"type": "integer"
|
||||
},
|
||||
"type": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"dto.UpdateTransactionRequest": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"amount": {
|
||||
"type": "number"
|
||||
},
|
||||
"category": {
|
||||
"type": "string"
|
||||
},
|
||||
"datetime": {
|
||||
"type": "string"
|
||||
},
|
||||
"description": {
|
||||
"type": "string"
|
||||
},
|
||||
"detach_receipt": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"receipt_id": {
|
||||
"type": "integer"
|
||||
},
|
||||
"type": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}`
|
||||
|
||||
@@ -238,6 +238,342 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/transactions": {
|
||||
"get": {
|
||||
"description": "Возвращает список транзакций с фильтрами и пагинацией",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"Transactions"
|
||||
],
|
||||
"summary": "Получить список транзакций",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "integer",
|
||||
"description": "Family ID",
|
||||
"name": "family_id",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"description": "User ID",
|
||||
"name": "created_by",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Transaction type",
|
||||
"name": "type",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Category",
|
||||
"name": "category",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "RFC3339 start datetime",
|
||||
"name": "date_from",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "RFC3339 end datetime",
|
||||
"name": "date_to",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"description": "Limit, default 50",
|
||||
"name": "limit",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"description": "Offset",
|
||||
"name": "offset",
|
||||
"in": "query"
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/dto.TransactionListResponse"
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Bad Request",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/dto.ErrorResponse"
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "Internal Server Error",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/dto.ErrorResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"post": {
|
||||
"description": "Создает новую транзакцию и при необходимости привязывает к ней чек",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"Transactions"
|
||||
],
|
||||
"summary": "Создать транзакцию",
|
||||
"parameters": [
|
||||
{
|
||||
"description": "Transaction payload",
|
||||
"name": "transaction",
|
||||
"in": "body",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/dto.CreateTransactionRequest"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"201": {
|
||||
"description": "Created",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/dto.TransactionResponse"
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Bad Request",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/dto.ErrorResponse"
|
||||
}
|
||||
},
|
||||
"404": {
|
||||
"description": "Not Found",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/dto.ErrorResponse"
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "Internal Server Error",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/dto.ErrorResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/transactions/{id}": {
|
||||
"get": {
|
||||
"description": "Возвращает транзакцию по ее внутреннему ID",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"Transactions"
|
||||
],
|
||||
"summary": "Получить транзакцию по ID",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "integer",
|
||||
"description": "Transaction ID",
|
||||
"name": "id",
|
||||
"in": "path",
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/dto.TransactionResponse"
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Bad Request",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/dto.ErrorResponse"
|
||||
}
|
||||
},
|
||||
"404": {
|
||||
"description": "Not Found",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/dto.ErrorResponse"
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "Internal Server Error",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/dto.ErrorResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"delete": {
|
||||
"description": "Удаляет транзакцию по ID",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"Transactions"
|
||||
],
|
||||
"summary": "Удалить транзакцию",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "integer",
|
||||
"description": "Transaction ID",
|
||||
"name": "id",
|
||||
"in": "path",
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"204": {
|
||||
"description": "no content",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Bad Request",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/dto.ErrorResponse"
|
||||
}
|
||||
},
|
||||
"404": {
|
||||
"description": "Not Found",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/dto.ErrorResponse"
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "Internal Server Error",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/dto.ErrorResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"patch": {
|
||||
"description": "Частично обновляет данные транзакции и связь с чеком",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"Transactions"
|
||||
],
|
||||
"summary": "Обновить транзакцию",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "integer",
|
||||
"description": "Transaction ID",
|
||||
"name": "id",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"description": "Transaction patch payload",
|
||||
"name": "transaction",
|
||||
"in": "body",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/dto.UpdateTransactionRequest"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/dto.TransactionResponse"
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Bad Request",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/dto.ErrorResponse"
|
||||
}
|
||||
},
|
||||
"404": {
|
||||
"description": "Not Found",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/dto.ErrorResponse"
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "Internal Server Error",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/dto.ErrorResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/users": {
|
||||
"post": {
|
||||
"consumes": [
|
||||
@@ -490,6 +826,55 @@
|
||||
}
|
||||
},
|
||||
"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": {
|
||||
@@ -563,6 +948,9 @@
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"telegram_chat_id": {
|
||||
"type": "integer"
|
||||
},
|
||||
"telegram_chat_name": {
|
||||
"type": "string"
|
||||
}
|
||||
@@ -621,6 +1009,123 @@
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"dto.CreateTransactionRequest": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"amount",
|
||||
"category",
|
||||
"created_by",
|
||||
"datetime",
|
||||
"family_id",
|
||||
"type"
|
||||
],
|
||||
"properties": {
|
||||
"amount": {
|
||||
"type": "number"
|
||||
},
|
||||
"category": {
|
||||
"type": "string"
|
||||
},
|
||||
"created_by": {
|
||||
"type": "integer"
|
||||
},
|
||||
"datetime": {
|
||||
"type": "string"
|
||||
},
|
||||
"description": {
|
||||
"type": "string"
|
||||
},
|
||||
"family_id": {
|
||||
"type": "integer"
|
||||
},
|
||||
"receipt_id": {
|
||||
"type": "integer"
|
||||
},
|
||||
"type": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"dto.ErrorResponse": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"message": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"dto.TransactionListResponse": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"items": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/dto.TransactionResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"dto.TransactionResponse": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"amount": {
|
||||
"type": "number"
|
||||
},
|
||||
"category": {
|
||||
"type": "string"
|
||||
},
|
||||
"created_at": {
|
||||
"type": "string"
|
||||
},
|
||||
"created_by": {
|
||||
"type": "integer"
|
||||
},
|
||||
"datetime": {
|
||||
"type": "string"
|
||||
},
|
||||
"description": {
|
||||
"type": "string"
|
||||
},
|
||||
"family_id": {
|
||||
"type": "integer"
|
||||
},
|
||||
"id": {
|
||||
"type": "integer"
|
||||
},
|
||||
"receipt_id": {
|
||||
"type": "integer"
|
||||
},
|
||||
"type": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"dto.UpdateTransactionRequest": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"amount": {
|
||||
"type": "number"
|
||||
},
|
||||
"category": {
|
||||
"type": "string"
|
||||
},
|
||||
"datetime": {
|
||||
"type": "string"
|
||||
},
|
||||
"description": {
|
||||
"type": "string"
|
||||
},
|
||||
"detach_receipt": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"receipt_id": {
|
||||
"type": "integer"
|
||||
},
|
||||
"type": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,37 @@
|
||||
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:
|
||||
@@ -47,6 +80,8 @@ definitions:
|
||||
properties:
|
||||
name:
|
||||
type: string
|
||||
telegram_chat_id:
|
||||
type: integer
|
||||
telegram_chat_name:
|
||||
type: string
|
||||
type: object
|
||||
@@ -85,6 +120,84 @@ definitions:
|
||||
username:
|
||||
type: string
|
||||
type: object
|
||||
dto.CreateTransactionRequest:
|
||||
properties:
|
||||
amount:
|
||||
type: number
|
||||
category:
|
||||
type: string
|
||||
created_by:
|
||||
type: integer
|
||||
datetime:
|
||||
type: string
|
||||
description:
|
||||
type: string
|
||||
family_id:
|
||||
type: integer
|
||||
receipt_id:
|
||||
type: integer
|
||||
type:
|
||||
type: string
|
||||
required:
|
||||
- amount
|
||||
- category
|
||||
- created_by
|
||||
- datetime
|
||||
- family_id
|
||||
- type
|
||||
type: object
|
||||
dto.ErrorResponse:
|
||||
properties:
|
||||
message:
|
||||
type: string
|
||||
type: object
|
||||
dto.TransactionListResponse:
|
||||
properties:
|
||||
items:
|
||||
items:
|
||||
$ref: '#/definitions/dto.TransactionResponse'
|
||||
type: array
|
||||
type: object
|
||||
dto.TransactionResponse:
|
||||
properties:
|
||||
amount:
|
||||
type: number
|
||||
category:
|
||||
type: string
|
||||
created_at:
|
||||
type: string
|
||||
created_by:
|
||||
type: integer
|
||||
datetime:
|
||||
type: string
|
||||
description:
|
||||
type: string
|
||||
family_id:
|
||||
type: integer
|
||||
id:
|
||||
type: integer
|
||||
receipt_id:
|
||||
type: integer
|
||||
type:
|
||||
type: string
|
||||
type: object
|
||||
dto.UpdateTransactionRequest:
|
||||
properties:
|
||||
amount:
|
||||
type: number
|
||||
category:
|
||||
type: string
|
||||
datetime:
|
||||
type: string
|
||||
description:
|
||||
type: string
|
||||
detach_receipt:
|
||||
type: boolean
|
||||
receipt_id:
|
||||
type: integer
|
||||
type:
|
||||
type: string
|
||||
type: object
|
||||
info:
|
||||
contact: {}
|
||||
paths:
|
||||
@@ -243,6 +356,230 @@ paths:
|
||||
summary: Обновить семью
|
||||
tags:
|
||||
- Families
|
||||
/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
|
||||
/transactions:
|
||||
get:
|
||||
consumes:
|
||||
- application/json
|
||||
description: Возвращает список транзакций с фильтрами и пагинацией
|
||||
parameters:
|
||||
- description: Family ID
|
||||
in: query
|
||||
name: family_id
|
||||
type: integer
|
||||
- description: User ID
|
||||
in: query
|
||||
name: created_by
|
||||
type: integer
|
||||
- description: Transaction type
|
||||
in: query
|
||||
name: type
|
||||
type: string
|
||||
- description: Category
|
||||
in: query
|
||||
name: category
|
||||
type: string
|
||||
- description: RFC3339 start datetime
|
||||
in: query
|
||||
name: date_from
|
||||
type: string
|
||||
- description: RFC3339 end datetime
|
||||
in: query
|
||||
name: date_to
|
||||
type: string
|
||||
- description: Limit, default 50
|
||||
in: query
|
||||
name: limit
|
||||
type: integer
|
||||
- description: Offset
|
||||
in: query
|
||||
name: offset
|
||||
type: integer
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: OK
|
||||
schema:
|
||||
$ref: '#/definitions/dto.TransactionListResponse'
|
||||
"400":
|
||||
description: Bad Request
|
||||
schema:
|
||||
$ref: '#/definitions/dto.ErrorResponse'
|
||||
"500":
|
||||
description: Internal Server Error
|
||||
schema:
|
||||
$ref: '#/definitions/dto.ErrorResponse'
|
||||
summary: Получить список транзакций
|
||||
tags:
|
||||
- Transactions
|
||||
post:
|
||||
consumes:
|
||||
- application/json
|
||||
description: Создает новую транзакцию и при необходимости привязывает к ней
|
||||
чек
|
||||
parameters:
|
||||
- description: Transaction payload
|
||||
in: body
|
||||
name: transaction
|
||||
required: true
|
||||
schema:
|
||||
$ref: '#/definitions/dto.CreateTransactionRequest'
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"201":
|
||||
description: Created
|
||||
schema:
|
||||
$ref: '#/definitions/dto.TransactionResponse'
|
||||
"400":
|
||||
description: Bad Request
|
||||
schema:
|
||||
$ref: '#/definitions/dto.ErrorResponse'
|
||||
"404":
|
||||
description: Not Found
|
||||
schema:
|
||||
$ref: '#/definitions/dto.ErrorResponse'
|
||||
"500":
|
||||
description: Internal Server Error
|
||||
schema:
|
||||
$ref: '#/definitions/dto.ErrorResponse'
|
||||
summary: Создать транзакцию
|
||||
tags:
|
||||
- Transactions
|
||||
/transactions/{id}:
|
||||
delete:
|
||||
consumes:
|
||||
- application/json
|
||||
description: Удаляет транзакцию по ID
|
||||
parameters:
|
||||
- description: Transaction ID
|
||||
in: path
|
||||
name: id
|
||||
required: true
|
||||
type: integer
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"204":
|
||||
description: no content
|
||||
schema:
|
||||
type: string
|
||||
"400":
|
||||
description: Bad Request
|
||||
schema:
|
||||
$ref: '#/definitions/dto.ErrorResponse'
|
||||
"404":
|
||||
description: Not Found
|
||||
schema:
|
||||
$ref: '#/definitions/dto.ErrorResponse'
|
||||
"500":
|
||||
description: Internal Server Error
|
||||
schema:
|
||||
$ref: '#/definitions/dto.ErrorResponse'
|
||||
summary: Удалить транзакцию
|
||||
tags:
|
||||
- Transactions
|
||||
get:
|
||||
consumes:
|
||||
- application/json
|
||||
description: Возвращает транзакцию по ее внутреннему ID
|
||||
parameters:
|
||||
- description: Transaction ID
|
||||
in: path
|
||||
name: id
|
||||
required: true
|
||||
type: integer
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: OK
|
||||
schema:
|
||||
$ref: '#/definitions/dto.TransactionResponse'
|
||||
"400":
|
||||
description: Bad Request
|
||||
schema:
|
||||
$ref: '#/definitions/dto.ErrorResponse'
|
||||
"404":
|
||||
description: Not Found
|
||||
schema:
|
||||
$ref: '#/definitions/dto.ErrorResponse'
|
||||
"500":
|
||||
description: Internal Server Error
|
||||
schema:
|
||||
$ref: '#/definitions/dto.ErrorResponse'
|
||||
summary: Получить транзакцию по ID
|
||||
tags:
|
||||
- Transactions
|
||||
patch:
|
||||
consumes:
|
||||
- application/json
|
||||
description: Частично обновляет данные транзакции и связь с чеком
|
||||
parameters:
|
||||
- description: Transaction ID
|
||||
in: path
|
||||
name: id
|
||||
required: true
|
||||
type: integer
|
||||
- description: Transaction patch payload
|
||||
in: body
|
||||
name: transaction
|
||||
required: true
|
||||
schema:
|
||||
$ref: '#/definitions/dto.UpdateTransactionRequest'
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: OK
|
||||
schema:
|
||||
$ref: '#/definitions/dto.TransactionResponse'
|
||||
"400":
|
||||
description: Bad Request
|
||||
schema:
|
||||
$ref: '#/definitions/dto.ErrorResponse'
|
||||
"404":
|
||||
description: Not Found
|
||||
schema:
|
||||
$ref: '#/definitions/dto.ErrorResponse'
|
||||
"500":
|
||||
description: Internal Server Error
|
||||
schema:
|
||||
$ref: '#/definitions/dto.ErrorResponse'
|
||||
summary: Обновить транзакцию
|
||||
tags:
|
||||
- Transactions
|
||||
/users:
|
||||
post:
|
||||
consumes:
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
package dto
|
||||
|
||||
import (
|
||||
"FamilyHub/src/domain"
|
||||
"time"
|
||||
)
|
||||
|
||||
type CreateTransactionRequest struct {
|
||||
FamilyID int64 `json:"family_id" binding:"required"`
|
||||
Description *string `json:"description"`
|
||||
Type string `json:"type" binding:"required"`
|
||||
DateTime string `json:"datetime" binding:"required"`
|
||||
Category string `json:"category" binding:"required"`
|
||||
Amount float64 `json:"amount" binding:"required"`
|
||||
CreatedBy int64 `json:"created_by" binding:"required"`
|
||||
ReceiptID *int64 `json:"receipt_id"`
|
||||
}
|
||||
|
||||
type UpdateTransactionRequest struct {
|
||||
Description *string `json:"description"`
|
||||
Type *string `json:"type"`
|
||||
DateTime *string `json:"datetime"`
|
||||
Category *string `json:"category"`
|
||||
Amount *float64 `json:"amount"`
|
||||
ReceiptID *int64 `json:"receipt_id"`
|
||||
DetachReceipt bool `json:"detach_receipt"`
|
||||
}
|
||||
|
||||
type ListTransactionsQuery struct {
|
||||
FamilyID *int64 `form:"family_id"`
|
||||
CreatedBy *int64 `form:"created_by"`
|
||||
Type *string `form:"type"`
|
||||
Category *string `form:"category"`
|
||||
DateFrom *string `form:"date_from"`
|
||||
DateTo *string `form:"date_to"`
|
||||
Limit int `form:"limit"`
|
||||
Offset int `form:"offset"`
|
||||
}
|
||||
|
||||
type TransactionResponse struct {
|
||||
ID int64 `json:"id"`
|
||||
FamilyID int64 `json:"family_id"`
|
||||
Description *string `json:"description"`
|
||||
Type string `json:"type"`
|
||||
DateTime string `json:"datetime"`
|
||||
Category string `json:"category"`
|
||||
Amount float64 `json:"amount"`
|
||||
CreatedAt string `json:"created_at"`
|
||||
CreatedBy int64 `json:"created_by"`
|
||||
ReceiptID *int64 `json:"receipt_id"`
|
||||
}
|
||||
|
||||
type TransactionListResponse struct {
|
||||
Items []TransactionResponse `json:"items"`
|
||||
}
|
||||
|
||||
func TransactionToResponse(transaction *domain.Transaction) TransactionResponse {
|
||||
return TransactionResponse{
|
||||
ID: transaction.ID,
|
||||
FamilyID: transaction.FamilyID,
|
||||
Description: transaction.Description,
|
||||
Type: transaction.Type,
|
||||
DateTime: transaction.DateTime.Format(time.RFC3339),
|
||||
Category: transaction.Category,
|
||||
Amount: transaction.Amount,
|
||||
CreatedAt: transaction.CreatedAt.Format(time.RFC3339),
|
||||
CreatedBy: transaction.CreatedBy,
|
||||
ReceiptID: transaction.ReceiptID,
|
||||
}
|
||||
}
|
||||
|
||||
func TransactionsToListResponse(transactions []*domain.Transaction) TransactionListResponse {
|
||||
items := make([]TransactionResponse, 0, len(transactions))
|
||||
for _, transaction := range transactions {
|
||||
items = append(items, TransactionToResponse(transaction))
|
||||
}
|
||||
|
||||
return TransactionListResponse{Items: items}
|
||||
}
|
||||
@@ -5,7 +5,9 @@ import (
|
||||
"FamilyHub/src/domain"
|
||||
"database/sql"
|
||||
"errors"
|
||||
"log"
|
||||
"net/http"
|
||||
"runtime/debug"
|
||||
"strconv"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
@@ -23,7 +25,7 @@ func (router *FamiliesRouter) RegisterRoutes(r *gin.RouterGroup) {
|
||||
families := r.Group("/families")
|
||||
{
|
||||
families.POST("", router.Create)
|
||||
families.GET("/:id", router.GetByID)
|
||||
families.GET("/:id", router.Read)
|
||||
families.PATCH("/:id", router.Update)
|
||||
families.DELETE("/:id", router.Delete)
|
||||
}
|
||||
@@ -58,7 +60,7 @@ func (router *FamiliesRouter) Create(c *gin.Context) {
|
||||
c.JSON(http.StatusCreated, resp.ModelToResponse(family))
|
||||
}
|
||||
|
||||
// GetByID GoDoc
|
||||
// Read GoDoc
|
||||
// @Summary Получить семью по ID
|
||||
// @Description Возвращает семью по ее внутреннему ID
|
||||
// @Tags Families
|
||||
@@ -70,7 +72,7 @@ func (router *FamiliesRouter) Create(c *gin.Context) {
|
||||
// @Failure 404 {object} map[string]string "family not found"
|
||||
// @Failure 500 {object} map[string]string "internal server error"
|
||||
// @Router /families/{id} [get]
|
||||
func (router *FamiliesRouter) GetByID(c *gin.Context) {
|
||||
func (router *FamiliesRouter) Read(c *gin.Context) {
|
||||
var resp domain.FamilyResponse
|
||||
|
||||
id, err := strconv.ParseInt(c.Param("id"), 10, 64)
|
||||
@@ -164,6 +166,14 @@ func handleFamilyError(c *gin.Context, err error) {
|
||||
case errors.Is(err, sql.ErrNoRows):
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "family not found"})
|
||||
default:
|
||||
log.Printf(
|
||||
"family request failed: method=%s path=%s route=%s error=%v\n%s",
|
||||
c.Request.Method,
|
||||
c.Request.URL.Path,
|
||||
c.FullPath(),
|
||||
err,
|
||||
debug.Stack(),
|
||||
)
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "internal server error"})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,6 +19,14 @@ import (
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func int64Ptr(v int64) *int64 {
|
||||
return &v
|
||||
}
|
||||
|
||||
func stringPtr(v string) *string {
|
||||
return &v
|
||||
}
|
||||
|
||||
type familyServiceMock struct {
|
||||
createFn func(ctx context.Context, req domain.CreateFamilyRequest) (*domain.Family, error)
|
||||
getByIDFn func(ctx context.Context, id int64) (*domain.Family, error)
|
||||
@@ -68,8 +76,8 @@ func sampleFamily() *domain.Family {
|
||||
ID: 7,
|
||||
Name: "Belan",
|
||||
OwnerID: 10,
|
||||
TelegramChatID: 12345,
|
||||
TelegramChatName: "Family Chat",
|
||||
TelegramChatID: int64Ptr(12345),
|
||||
TelegramChatName: stringPtr("Family Chat"),
|
||||
CreatedAt: time.Date(2026, time.January, 21, 10, 0, 0, 0, time.UTC),
|
||||
UpdatedAt: time.Date(2026, time.January, 21, 11, 0, 0, 0, time.UTC),
|
||||
}
|
||||
@@ -106,9 +114,11 @@ func TestFamiliesRouter_Create(t *testing.T) {
|
||||
expected := sampleFamily()
|
||||
r := setupFamiliesRouter(&familyServiceMock{createFn: func(ctx context.Context, req domain.CreateFamilyRequest) (*domain.Family, error) {
|
||||
assert.Equal(t, "Belan", req.Name)
|
||||
assert.Nil(t, req.TelegramChatID)
|
||||
assert.Nil(t, req.TelegramChatName)
|
||||
return expected, nil
|
||||
}})
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/v1/families", bytes.NewBufferString(`{"name":"Belan","owner_id":10,"telegram_chat_id":12345,"telegram_chat_name":"Family Chat"}`))
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/v1/families", bytes.NewBufferString(`{"name":"Belan","owner_id":10}`))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
@@ -233,7 +243,8 @@ func TestFamiliesRouter_Update(t *testing.T) {
|
||||
assert.Equal(t, int64(7), id)
|
||||
require.NotNil(t, req.Name)
|
||||
assert.Equal(t, updatedName, *req.Name)
|
||||
assert.Equal(t, "Updated", req.TelegramChatName)
|
||||
require.NotNil(t, req.TelegramChatName)
|
||||
assert.Equal(t, "Updated", *req.TelegramChatName)
|
||||
return expected, nil
|
||||
}})
|
||||
req := httptest.NewRequest(http.MethodPatch, "/api/v1/families/7", bytes.NewBufferString(`{"name":"`+updatedName+`","telegram_chat_name":"Updated"}`))
|
||||
|
||||
@@ -13,7 +13,7 @@ import (
|
||||
)
|
||||
|
||||
type receiptService interface {
|
||||
GetReceipt(ctx context.Context, date string, number string) (*domain.Receipt, error)
|
||||
GetReceipt(ctx context.Context, req domain.AddReceiptRequest) (*domain.Receipt, error)
|
||||
}
|
||||
|
||||
type ReceiptRouter struct {
|
||||
@@ -30,6 +30,17 @@ func (router *ReceiptRouter) RegisterRoutes(r *gin.RouterGroup) {
|
||||
}
|
||||
}
|
||||
|
||||
// AddReceipt GoDoc
|
||||
// @Summary Загрузить чек
|
||||
// @Description Загружает чек из внешнего сервиса и опционально автоматически создает связанную транзакцию
|
||||
// @Tags Receipts
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param receipt body domain.AddReceiptRequest true "Receipt payload"
|
||||
// @Success 200 {object} domain.AddReceiptResponse
|
||||
// @Failure 400 {object} dto.ErrorResponse
|
||||
// @Failure 500 {object} dto.ErrorResponse
|
||||
// @Router /receipts [post]
|
||||
func (router *ReceiptRouter) AddReceipt(context_ *gin.Context) {
|
||||
var req domain.AddReceiptRequest
|
||||
if err := context_.ShouldBindJSON(&req); err != nil {
|
||||
@@ -46,7 +57,9 @@ func (router *ReceiptRouter) AddReceipt(context_ *gin.Context) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
|
||||
defer cancel()
|
||||
|
||||
receipt, err := router.service.GetReceipt(ctx, isoDate, req.Number)
|
||||
req.Date = isoDate
|
||||
|
||||
receipt, err := router.service.GetReceipt(ctx, req)
|
||||
if err != nil {
|
||||
context_.JSON(400, gin.H{"error": err.Error()})
|
||||
log.Printf("API error, %s", err.Error())
|
||||
@@ -54,9 +67,10 @@ func (router *ReceiptRouter) AddReceipt(context_ *gin.Context) {
|
||||
}
|
||||
|
||||
resp := domain.AddReceiptResponse{
|
||||
ID: 1,
|
||||
Number: receipt.ReceiptNumber,
|
||||
Date: receipt.IssuedAt,
|
||||
ID: int32(receipt.ID),
|
||||
Number: receipt.ReceiptNumber,
|
||||
Date: receipt.IssuedAt,
|
||||
TransactionID: receipt.TransactionID,
|
||||
}
|
||||
context_.JSON(http.StatusOK, resp)
|
||||
}
|
||||
|
||||
@@ -18,12 +18,12 @@ import (
|
||||
)
|
||||
|
||||
type receiptServiceMock struct {
|
||||
getReceiptFn func(ctx context.Context, date string, number string) (*domain.Receipt, error)
|
||||
getReceiptFn func(ctx context.Context, req domain.AddReceiptRequest) (*domain.Receipt, error)
|
||||
}
|
||||
|
||||
func (m *receiptServiceMock) GetReceipt(ctx context.Context, date string, number string) (*domain.Receipt, error) {
|
||||
func (m *receiptServiceMock) GetReceipt(ctx context.Context, req domain.AddReceiptRequest) (*domain.Receipt, error) {
|
||||
if m.getReceiptFn != nil {
|
||||
return m.getReceiptFn(ctx, date, number)
|
||||
return m.getReceiptFn(ctx, req)
|
||||
}
|
||||
return nil, errors.New("mock is not configured")
|
||||
}
|
||||
@@ -60,9 +60,9 @@ func TestReceiptRouter_AddReceipt(t *testing.T) {
|
||||
{
|
||||
name: "bad request on service error",
|
||||
body: `{"number":"` + validNumber + `","date":"` + validDate + `"}`,
|
||||
mock: &receiptServiceMock{getReceiptFn: func(ctx context.Context, date string, number string) (*domain.Receipt, error) {
|
||||
assert.Equal(t, expectedDate, date)
|
||||
assert.Equal(t, validNumber, number)
|
||||
mock: &receiptServiceMock{getReceiptFn: func(ctx context.Context, req domain.AddReceiptRequest) (*domain.Receipt, error) {
|
||||
assert.Equal(t, expectedDate, req.Date)
|
||||
assert.Equal(t, validNumber, req.Number)
|
||||
return nil, errors.New("receipt not found")
|
||||
}},
|
||||
expectedStatus: http.StatusBadRequest,
|
||||
@@ -71,10 +71,10 @@ func TestReceiptRouter_AddReceipt(t *testing.T) {
|
||||
{
|
||||
name: "ok",
|
||||
body: `{"number":"` + validNumber + `","date":"` + validDate + `"}`,
|
||||
mock: &receiptServiceMock{getReceiptFn: func(ctx context.Context, date string, number string) (*domain.Receipt, error) {
|
||||
assert.Equal(t, expectedDate, date)
|
||||
assert.Equal(t, validNumber, number)
|
||||
return &domain.Receipt{ReceiptNumber: validNumber, IssuedAt: now}, nil
|
||||
mock: &receiptServiceMock{getReceiptFn: func(ctx context.Context, req domain.AddReceiptRequest) (*domain.Receipt, error) {
|
||||
assert.Equal(t, expectedDate, req.Date)
|
||||
assert.Equal(t, validNumber, req.Number)
|
||||
return &domain.Receipt{ID: 7, ReceiptNumber: validNumber, IssuedAt: now}, nil
|
||||
}},
|
||||
expectedStatus: http.StatusOK,
|
||||
expectedContains: validNumber,
|
||||
@@ -108,7 +108,7 @@ func TestReceiptRouter_AddReceipt(t *testing.T) {
|
||||
}
|
||||
err := json.Unmarshal(w.Body.Bytes(), &resp)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, int32(1), resp.ID)
|
||||
assert.Equal(t, int32(7), resp.ID)
|
||||
assert.Equal(t, validNumber, resp.Number)
|
||||
assert.Equal(t, now, resp.Date)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,266 @@
|
||||
package routers
|
||||
|
||||
import (
|
||||
"FamilyHub/src/api/dto"
|
||||
"FamilyHub/src/api/services"
|
||||
"FamilyHub/src/domain"
|
||||
"errors"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type TransactionsRouter struct {
|
||||
service services.TransactionService
|
||||
}
|
||||
|
||||
func NewTransactionsRouter(s services.TransactionService) *TransactionsRouter {
|
||||
return &TransactionsRouter{service: s}
|
||||
}
|
||||
|
||||
func (router *TransactionsRouter) RegisterRoutes(r *gin.RouterGroup) {
|
||||
transactions := r.Group("/transactions")
|
||||
{
|
||||
transactions.POST("", router.Create)
|
||||
transactions.GET("", router.List)
|
||||
transactions.GET("/:id", router.Read)
|
||||
transactions.PATCH("/:id", router.Update)
|
||||
transactions.DELETE("/:id", router.Delete)
|
||||
}
|
||||
}
|
||||
|
||||
// Create GoDoc
|
||||
// @Summary Создать транзакцию
|
||||
// @Description Создает новую транзакцию и при необходимости привязывает к ней чек
|
||||
// @Tags Transactions
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param transaction body dto.CreateTransactionRequest true "Transaction payload"
|
||||
// @Success 201 {object} dto.TransactionResponse
|
||||
// @Failure 400 {object} dto.ErrorResponse
|
||||
// @Failure 404 {object} dto.ErrorResponse
|
||||
// @Failure 500 {object} dto.ErrorResponse
|
||||
// @Router /transactions [post]
|
||||
func (router *TransactionsRouter) Create(c *gin.Context) {
|
||||
var req dto.CreateTransactionRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, dto.ErrorResponse{Message: err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
dateTime, err := time.Parse(time.RFC3339, req.DateTime)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, dto.ErrorResponse{Message: "datetime must be RFC3339"})
|
||||
return
|
||||
}
|
||||
|
||||
transaction, err := router.service.Create(c.Request.Context(), domain.CreateTransactionRequest{
|
||||
FamilyID: req.FamilyID,
|
||||
Description: req.Description,
|
||||
Type: req.Type,
|
||||
DateTime: dateTime,
|
||||
Category: req.Category,
|
||||
Amount: req.Amount,
|
||||
CreatedBy: req.CreatedBy,
|
||||
ReceiptID: req.ReceiptID,
|
||||
})
|
||||
if err != nil {
|
||||
handleTransactionError(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusCreated, dto.TransactionToResponse(transaction))
|
||||
}
|
||||
|
||||
// List GoDoc
|
||||
// @Summary Получить список транзакций
|
||||
// @Description Возвращает список транзакций с фильтрами и пагинацией
|
||||
// @Tags Transactions
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param family_id query int false "Family ID"
|
||||
// @Param created_by query int false "User ID"
|
||||
// @Param type query string false "Transaction type"
|
||||
// @Param category query string false "Category"
|
||||
// @Param date_from query string false "RFC3339 start datetime"
|
||||
// @Param date_to query string false "RFC3339 end datetime"
|
||||
// @Param limit query int false "Limit, default 50"
|
||||
// @Param offset query int false "Offset"
|
||||
// @Success 200 {object} dto.TransactionListResponse
|
||||
// @Failure 400 {object} dto.ErrorResponse
|
||||
// @Failure 500 {object} dto.ErrorResponse
|
||||
// @Router /transactions [get]
|
||||
func (router *TransactionsRouter) List(c *gin.Context) {
|
||||
var query dto.ListTransactionsQuery
|
||||
if err := c.ShouldBindQuery(&query); err != nil {
|
||||
c.JSON(http.StatusBadRequest, dto.ErrorResponse{Message: err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
filter, err := transactionQueryToFilter(query)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, dto.ErrorResponse{Message: err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
transactions, err := router.service.List(c.Request.Context(), filter)
|
||||
if err != nil {
|
||||
handleTransactionError(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, dto.TransactionsToListResponse(transactions))
|
||||
}
|
||||
|
||||
// Read GoDoc
|
||||
// @Summary Получить транзакцию по ID
|
||||
// @Description Возвращает транзакцию по ее внутреннему ID
|
||||
// @Tags Transactions
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param id path int true "Transaction ID"
|
||||
// @Success 200 {object} dto.TransactionResponse
|
||||
// @Failure 400 {object} dto.ErrorResponse
|
||||
// @Failure 404 {object} dto.ErrorResponse
|
||||
// @Failure 500 {object} dto.ErrorResponse
|
||||
// @Router /transactions/{id} [get]
|
||||
func (router *TransactionsRouter) Read(c *gin.Context) {
|
||||
id, err := strconv.ParseInt(c.Param("id"), 10, 64)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, dto.ErrorResponse{Message: "invalid id"})
|
||||
return
|
||||
}
|
||||
|
||||
transaction, err := router.service.GetByID(c.Request.Context(), id)
|
||||
if err != nil {
|
||||
handleTransactionError(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, dto.TransactionToResponse(transaction))
|
||||
}
|
||||
|
||||
// Update GoDoc
|
||||
// @Summary Обновить транзакцию
|
||||
// @Description Частично обновляет данные транзакции и связь с чеком
|
||||
// @Tags Transactions
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param id path int true "Transaction ID"
|
||||
// @Param transaction body dto.UpdateTransactionRequest true "Transaction patch payload"
|
||||
// @Success 200 {object} dto.TransactionResponse
|
||||
// @Failure 400 {object} dto.ErrorResponse
|
||||
// @Failure 404 {object} dto.ErrorResponse
|
||||
// @Failure 500 {object} dto.ErrorResponse
|
||||
// @Router /transactions/{id} [patch]
|
||||
func (router *TransactionsRouter) Update(c *gin.Context) {
|
||||
id, err := strconv.ParseInt(c.Param("id"), 10, 64)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, dto.ErrorResponse{Message: "invalid id"})
|
||||
return
|
||||
}
|
||||
|
||||
var req dto.UpdateTransactionRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, dto.ErrorResponse{Message: err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
var dateTime *time.Time
|
||||
if req.DateTime != nil {
|
||||
parsed, err := time.Parse(time.RFC3339, *req.DateTime)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, dto.ErrorResponse{Message: "datetime must be RFC3339"})
|
||||
return
|
||||
}
|
||||
dateTime = &parsed
|
||||
}
|
||||
|
||||
transaction, err := router.service.Update(c.Request.Context(), id, domain.UpdateTransactionRequest{
|
||||
Description: req.Description,
|
||||
Type: req.Type,
|
||||
DateTime: dateTime,
|
||||
Category: req.Category,
|
||||
Amount: req.Amount,
|
||||
ReceiptID: req.ReceiptID,
|
||||
DetachReceipt: req.DetachReceipt,
|
||||
})
|
||||
if err != nil {
|
||||
handleTransactionError(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, dto.TransactionToResponse(transaction))
|
||||
}
|
||||
|
||||
// Delete GoDoc
|
||||
// @Summary Удалить транзакцию
|
||||
// @Description Удаляет транзакцию по ID
|
||||
// @Tags Transactions
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param id path int true "Transaction ID"
|
||||
// @Success 204 {string} string "no content"
|
||||
// @Failure 400 {object} dto.ErrorResponse
|
||||
// @Failure 404 {object} dto.ErrorResponse
|
||||
// @Failure 500 {object} dto.ErrorResponse
|
||||
// @Router /transactions/{id} [delete]
|
||||
func (router *TransactionsRouter) Delete(c *gin.Context) {
|
||||
id, err := strconv.ParseInt(c.Param("id"), 10, 64)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, dto.ErrorResponse{Message: "invalid id"})
|
||||
return
|
||||
}
|
||||
|
||||
if err := router.service.Delete(c.Request.Context(), id); err != nil {
|
||||
handleTransactionError(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
c.Status(http.StatusNoContent)
|
||||
}
|
||||
|
||||
func handleTransactionError(c *gin.Context, err error) {
|
||||
switch {
|
||||
case errors.Is(err, services.ErrTransactionNotFound):
|
||||
c.JSON(http.StatusNotFound, dto.ErrorResponse{Message: err.Error()})
|
||||
case errors.Is(err, services.ErrTransactionPatch),
|
||||
errors.Is(err, services.ErrReceiptLinkConflict),
|
||||
errors.Is(err, services.ErrInvalidTransaction):
|
||||
c.JSON(http.StatusBadRequest, dto.ErrorResponse{Message: err.Error()})
|
||||
case errors.Is(err, services.ErrReceiptNotFound):
|
||||
c.JSON(http.StatusNotFound, dto.ErrorResponse{Message: err.Error()})
|
||||
default:
|
||||
c.JSON(http.StatusInternalServerError, dto.ErrorResponse{Message: "internal server error"})
|
||||
}
|
||||
}
|
||||
|
||||
func transactionQueryToFilter(query dto.ListTransactionsQuery) (domain.TransactionListFilter, error) {
|
||||
filter := domain.TransactionListFilter{
|
||||
FamilyID: query.FamilyID,
|
||||
CreatedBy: query.CreatedBy,
|
||||
Type: query.Type,
|
||||
Category: query.Category,
|
||||
Limit: query.Limit,
|
||||
Offset: query.Offset,
|
||||
}
|
||||
|
||||
if query.DateFrom != nil {
|
||||
parsed, err := time.Parse(time.RFC3339, *query.DateFrom)
|
||||
if err != nil {
|
||||
return domain.TransactionListFilter{}, errors.New("date_from must be RFC3339")
|
||||
}
|
||||
filter.DateFrom = &parsed
|
||||
}
|
||||
if query.DateTo != nil {
|
||||
parsed, err := time.Parse(time.RFC3339, *query.DateTo)
|
||||
if err != nil {
|
||||
return domain.TransactionListFilter{}, errors.New("date_to must be RFC3339")
|
||||
}
|
||||
filter.DateTo = &parsed
|
||||
}
|
||||
|
||||
return filter, nil
|
||||
}
|
||||
@@ -21,15 +21,15 @@ func NewUsersRouter(s services.UserService) *UsersRouter {
|
||||
func (router *UsersRouter) RegisterRoutes(r *gin.RouterGroup) {
|
||||
users := r.Group("/users")
|
||||
{
|
||||
users.POST("", router.CreateUser)
|
||||
users.GET("/:id", router.GetByID)
|
||||
users.GET("/by-telegram/:telegramId", router.GetByTelegramID)
|
||||
users.POST("", router.Create)
|
||||
users.GET("/:id", router.Read)
|
||||
users.PATCH("/:id", router.Update)
|
||||
users.DELETE("/:id", router.Delete)
|
||||
users.GET("/by-telegram/:telegramId", router.GetByTelegramID)
|
||||
}
|
||||
}
|
||||
|
||||
// CreateUser GoDoc
|
||||
// Create GoDoc
|
||||
// @Summary Создать пользователя
|
||||
// @Tags Users
|
||||
// @Accept json
|
||||
@@ -39,7 +39,7 @@ func (router *UsersRouter) RegisterRoutes(r *gin.RouterGroup) {
|
||||
// @Failure 400 {object} domain.UserErrorResponse
|
||||
// @Failure 500 {object} domain.UserErrorResponse
|
||||
// @Router /users [post]
|
||||
func (router *UsersRouter) CreateUser(c *gin.Context) {
|
||||
func (router *UsersRouter) Create(c *gin.Context) {
|
||||
var req domain.CreateUserRequest
|
||||
var resp domain.UserResponse
|
||||
|
||||
@@ -57,7 +57,7 @@ func (router *UsersRouter) CreateUser(c *gin.Context) {
|
||||
c.JSON(http.StatusCreated, resp.ModelToResponse(user))
|
||||
}
|
||||
|
||||
// GetByID GoDoc
|
||||
// Read GoDoc
|
||||
// @Summary Получить пользователя по ID
|
||||
// @Description Возвращает пользователя по его внутреннему ID
|
||||
// @Tags Users
|
||||
@@ -69,7 +69,7 @@ func (router *UsersRouter) CreateUser(c *gin.Context) {
|
||||
// @Failure 404 {object} domain.UserErrorResponse "user not found"
|
||||
// @Failure 500 {object} domain.UserErrorResponse "internal server error"
|
||||
// @Router /users/{id} [get]
|
||||
func (router *UsersRouter) GetByID(c *gin.Context) {
|
||||
func (router *UsersRouter) Read(c *gin.Context) {
|
||||
var resp domain.UserResponse
|
||||
id, err := strconv.ParseInt(c.Param("id"), 10, 64)
|
||||
if err != nil {
|
||||
|
||||
@@ -12,6 +12,7 @@ import (
|
||||
"log"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
@@ -36,8 +37,10 @@ func NewServer(cfg config.Config) *Server {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
gin.SetMode(gin.ReleaseMode)
|
||||
router := gin.Default()
|
||||
//gin.SetMode(gin.ReleaseMode)
|
||||
router := gin.New()
|
||||
router.Use(gin.Logger())
|
||||
router.Use(gin.RecoveryWithWriter(os.Stderr))
|
||||
if cfg.OpenAPIEnabled {
|
||||
openAPIEndpoint := cfg.OpenAPIEndpoint
|
||||
if openAPIEndpoint == "" {
|
||||
@@ -84,11 +87,17 @@ func NewServer(cfg config.Config) *Server {
|
||||
|
||||
apiV1 := router.Group("/api/v1")
|
||||
|
||||
transactionRepo := repositories.NewTransactionsSQLRepository(dbConn)
|
||||
|
||||
receiptRepo := repositories.NewReceiptsSQLRepository(dbConn)
|
||||
receiptService_ := receiptService.NewReceiptService(receiptRepo)
|
||||
receiptService_ := receiptService.NewReceiptService(receiptRepo, transactionRepo)
|
||||
receiptRouter := routers.NewReceiptRouter(receiptService_)
|
||||
receiptRouter.RegisterRoutes(apiV1)
|
||||
|
||||
transactionService := services.NewTransactionService(transactionRepo)
|
||||
transactionRouter := routers.NewTransactionsRouter(transactionService)
|
||||
transactionRouter.RegisterRoutes(apiV1)
|
||||
|
||||
usersRepo := repositories.NewUsersSQLRepository(dbConn)
|
||||
usersService := services.NewUserService(usersRepo)
|
||||
usersRouter := routers.NewUsersRouter(usersService)
|
||||
|
||||
@@ -61,7 +61,7 @@ func (s *familyService) Update(ctx context.Context, id int64, req domain.UpdateF
|
||||
ID: id,
|
||||
Name: *req.Name,
|
||||
OwnerID: existing.OwnerID,
|
||||
TelegramChatID: existing.TelegramChatID,
|
||||
TelegramChatID: req.TelegramChatID,
|
||||
TelegramChatName: req.TelegramChatName,
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
|
||||
@@ -0,0 +1,177 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"FamilyHub/src/domain"
|
||||
"FamilyHub/src/repositories"
|
||||
"context"
|
||||
"database/sql"
|
||||
"errors"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type TransactionService interface {
|
||||
Create(ctx context.Context, req domain.CreateTransactionRequest) (*domain.Transaction, error)
|
||||
GetByID(ctx context.Context, id int64) (*domain.Transaction, error)
|
||||
List(ctx context.Context, filter domain.TransactionListFilter) ([]*domain.Transaction, error)
|
||||
Update(ctx context.Context, id int64, req domain.UpdateTransactionRequest) (*domain.Transaction, error)
|
||||
Delete(ctx context.Context, id int64) error
|
||||
}
|
||||
|
||||
type transactionService struct {
|
||||
repo repositories.TransactionRepository
|
||||
}
|
||||
|
||||
func NewTransactionService(repo repositories.TransactionRepository) TransactionService {
|
||||
return &transactionService{repo: repo}
|
||||
}
|
||||
|
||||
var (
|
||||
ErrTransactionNotFound = errors.New("transaction not found")
|
||||
ErrTransactionPatch = errors.New("empty update payload")
|
||||
ErrReceiptLinkConflict = errors.New("receipt_id and detach_receipt cannot be used together")
|
||||
ErrInvalidTransaction = errors.New("type and category are required")
|
||||
ErrReceiptNotFound = errors.New("receipt not found")
|
||||
)
|
||||
|
||||
func (s *transactionService) Create(ctx context.Context, req domain.CreateTransactionRequest) (*domain.Transaction, error) {
|
||||
if strings.TrimSpace(req.Type) == "" || strings.TrimSpace(req.Category) == "" {
|
||||
return nil, ErrInvalidTransaction
|
||||
}
|
||||
|
||||
transaction := &domain.Transaction{
|
||||
FamilyID: req.FamilyID,
|
||||
Description: req.Description,
|
||||
Type: req.Type,
|
||||
DateTime: req.DateTime,
|
||||
Category: req.Category,
|
||||
Amount: req.Amount,
|
||||
CreatedBy: req.CreatedBy,
|
||||
ReceiptID: req.ReceiptID,
|
||||
}
|
||||
|
||||
if err := s.repo.Create(ctx, transaction); err != nil {
|
||||
if errors.Is(err, repositories.ErrReceiptNotFound) {
|
||||
return nil, ErrReceiptNotFound
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return transaction, nil
|
||||
}
|
||||
|
||||
func (s *transactionService) GetByID(ctx context.Context, id int64) (*domain.Transaction, error) {
|
||||
transaction, err := s.repo.GetByID(ctx, id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if transaction == nil {
|
||||
return nil, ErrTransactionNotFound
|
||||
}
|
||||
|
||||
return transaction, nil
|
||||
}
|
||||
|
||||
func (s *transactionService) List(ctx context.Context, filter domain.TransactionListFilter) ([]*domain.Transaction, error) {
|
||||
if filter.Limit <= 0 {
|
||||
filter.Limit = 50
|
||||
}
|
||||
if filter.Limit > 200 {
|
||||
filter.Limit = 200
|
||||
}
|
||||
if filter.Offset < 0 {
|
||||
filter.Offset = 0
|
||||
}
|
||||
|
||||
return s.repo.List(ctx, filter)
|
||||
}
|
||||
|
||||
func (s *transactionService) Update(ctx context.Context, id int64, req domain.UpdateTransactionRequest) (*domain.Transaction, error) {
|
||||
if req.ReceiptID != nil && req.DetachReceipt {
|
||||
return nil, ErrReceiptLinkConflict
|
||||
}
|
||||
|
||||
existing, err := s.repo.GetByID(ctx, id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if existing == nil {
|
||||
return nil, ErrTransactionNotFound
|
||||
}
|
||||
|
||||
if req.Description == nil &&
|
||||
req.Type == nil &&
|
||||
req.DateTime == nil &&
|
||||
req.Category == nil &&
|
||||
req.Amount == nil &&
|
||||
req.ReceiptID == nil &&
|
||||
!req.DetachReceipt {
|
||||
return nil, ErrTransactionPatch
|
||||
}
|
||||
|
||||
updated := &domain.Transaction{
|
||||
ID: existing.ID,
|
||||
FamilyID: existing.FamilyID,
|
||||
Description: existing.Description,
|
||||
Type: existing.Type,
|
||||
DateTime: existing.DateTime,
|
||||
Category: existing.Category,
|
||||
Amount: existing.Amount,
|
||||
CreatedAt: existing.CreatedAt,
|
||||
CreatedBy: existing.CreatedBy,
|
||||
ReceiptID: existing.ReceiptID,
|
||||
}
|
||||
|
||||
if req.Description != nil {
|
||||
updated.Description = req.Description
|
||||
}
|
||||
if req.Type != nil {
|
||||
updated.Type = *req.Type
|
||||
}
|
||||
if req.DateTime != nil {
|
||||
updated.DateTime = *req.DateTime
|
||||
}
|
||||
if req.Category != nil {
|
||||
updated.Category = *req.Category
|
||||
}
|
||||
if req.Amount != nil {
|
||||
updated.Amount = *req.Amount
|
||||
}
|
||||
|
||||
syncReceipt := false
|
||||
if req.DetachReceipt {
|
||||
updated.ReceiptID = nil
|
||||
syncReceipt = true
|
||||
}
|
||||
if req.ReceiptID != nil {
|
||||
updated.ReceiptID = req.ReceiptID
|
||||
syncReceipt = true
|
||||
}
|
||||
|
||||
if strings.TrimSpace(updated.Type) == "" || strings.TrimSpace(updated.Category) == "" {
|
||||
return nil, ErrInvalidTransaction
|
||||
}
|
||||
|
||||
if err := s.repo.Update(ctx, updated, syncReceipt); err != nil {
|
||||
if errors.Is(err, repositories.ErrReceiptNotFound) {
|
||||
return nil, ErrReceiptNotFound
|
||||
}
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return nil, ErrTransactionNotFound
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return s.repo.GetByID(ctx, id)
|
||||
}
|
||||
|
||||
func (s *transactionService) Delete(ctx context.Context, id int64) error {
|
||||
transaction, err := s.repo.GetByID(ctx, id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if transaction == nil {
|
||||
return ErrTransactionNotFound
|
||||
}
|
||||
|
||||
return s.repo.Delete(ctx, id)
|
||||
}
|
||||
Reference in New Issue
Block a user