Added transaction feature, fixed some mistakes
This commit is contained in:
@@ -6,3 +6,4 @@ data
|
|||||||
archive
|
archive
|
||||||
volumes
|
volumes
|
||||||
*.dtmp
|
*.dtmp
|
||||||
|
*.gocache
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
CREATE TABLE users
|
CREATE TABLE users
|
||||||
(
|
(
|
||||||
id BIGSERIAL PRIMARY KEY,
|
id BIGSERIAL PRIMARY KEY,
|
||||||
telegram_id BIGINT UNIQUE NOT NULL,
|
telegram_id BIGINT UNIQUE,
|
||||||
username TEXT,
|
username TEXT,
|
||||||
first_name TEXT NOT NULL,
|
first_name TEXT NOT NULL,
|
||||||
last_name TEXT,
|
last_name TEXT,
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ CREATE TABLE families
|
|||||||
id BIGSERIAL PRIMARY KEY,
|
id BIGSERIAL PRIMARY KEY,
|
||||||
name TEXT NOT NULL,
|
name TEXT NOT NULL,
|
||||||
owner_id BIGINT NOT NULL REFERENCES users (id),
|
owner_id BIGINT NOT NULL REFERENCES users (id),
|
||||||
telegram_chat_id BIGINT NOT NULL,
|
telegram_chat_id BIGINT,
|
||||||
telegram_chat_name TEXT,
|
telegram_chat_name TEXT,
|
||||||
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
|
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
|
||||||
updated_at TIMESTAMP NOT NULL DEFAULT NOW()
|
updated_at TIMESTAMP NOT NULL DEFAULT NOW()
|
||||||
|
|||||||
@@ -1,2 +0,0 @@
|
|||||||
DROP TABLE IF EXISTS threads;
|
|
||||||
DROP TYPE IF EXISTS thread_type;
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
CREATE TABLE family_threads
|
|
||||||
(
|
|
||||||
id BIGSERIAL PRIMARY KEY,
|
|
||||||
family_id BIGINT NOT NULL REFERENCES families (id) ON DELETE CASCADE,
|
|
||||||
type TEXT NOT NULL,
|
|
||||||
title TEXT NOT NULL,
|
|
||||||
telegram_topic_id BIGINT NOT NULL,
|
|
||||||
is_system BOOLEAN NOT NULL DEFAULT FALSE,
|
|
||||||
created_by BIGINT NOT NULL REFERENCES users (id),
|
|
||||||
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
|
|
||||||
|
|
||||||
UNIQUE (family_id, telegram_topic_id)
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE UNIQUE INDEX idx_unique_system_threads ON family_threads (family_id, type) WHERE is_system = TRUE;
|
|
||||||
|
|
||||||
CREATE INDEX idx_threads_family_id ON family_threads (family_id);
|
|
||||||
|
|
||||||
CREATE INDEX idx_threads_family_type ON family_threads (family_id, type);
|
|
||||||
@@ -1 +1,2 @@
|
|||||||
DROP TABLE receipts;
|
DROP TABLE IF EXISTS receipts;
|
||||||
|
DROP TABLE IF EXISTS transactions;
|
||||||
|
|||||||
@@ -1,6 +1,25 @@
|
|||||||
|
CREATE TABLE transactions
|
||||||
|
(
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
family_id BIGINT NOT NULL REFERENCES families (id) ON DELETE CASCADE,
|
||||||
|
description TEXT,
|
||||||
|
type TEXT NOT NULL,
|
||||||
|
datetime TIMESTAMP NOT NULL,
|
||||||
|
category TEXT NOT NULL,
|
||||||
|
amount NUMERIC(14, 2) NOT NULL,
|
||||||
|
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
|
||||||
|
created_by BIGINT NOT NULL REFERENCES users (id) ON DELETE RESTRICT
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX idx_transactions_family_id ON transactions (family_id);
|
||||||
|
CREATE INDEX idx_transactions_datetime ON transactions (datetime);
|
||||||
|
CREATE INDEX idx_transactions_created_by ON transactions (created_by);
|
||||||
|
CREATE INDEX idx_transactions_family_datetime ON transactions (family_id, datetime DESC);
|
||||||
|
|
||||||
CREATE TABLE receipts
|
CREATE TABLE receipts
|
||||||
(
|
(
|
||||||
id BIGSERIAL PRIMARY KEY,
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
transaction_id BIGINT UNIQUE REFERENCES transactions (id) ON DELETE SET NULL,
|
||||||
receipt_number TEXT NOT NULL UNIQUE,
|
receipt_number TEXT NOT NULL UNIQUE,
|
||||||
ui TEXT NOT NULL,
|
ui TEXT NOT NULL,
|
||||||
status INTEGER NOT NULL,
|
status INTEGER NOT NULL,
|
||||||
@@ -29,7 +48,8 @@ CREATE TABLE receipts
|
|||||||
skno_number TEXT,
|
skno_number TEXT,
|
||||||
unp TEXT,
|
unp TEXT,
|
||||||
success TEXT,
|
success TEXT,
|
||||||
created_at TIMESTAMP DEFAULT NOW()
|
created_at TIMESTAMP NOT NULL DEFAULT NOW()
|
||||||
);
|
);
|
||||||
|
|
||||||
CREATE INDEX idx_receipts_issued_at ON receipts(issued_at);
|
CREATE INDEX idx_receipts_issued_at ON receipts (issued_at);
|
||||||
|
CREATE INDEX idx_receipts_transaction_id ON receipts (transaction_id);
|
||||||
|
|||||||
@@ -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": {
|
"/users": {
|
||||||
"post": {
|
"post": {
|
||||||
"consumes": [
|
"consumes": [
|
||||||
@@ -501,6 +837,55 @@ 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": {
|
||||||
@@ -574,6 +959,9 @@ const docTemplate = `{
|
|||||||
"name": {
|
"name": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
|
"telegram_chat_id": {
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
"telegram_chat_name": {
|
"telegram_chat_name": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
}
|
}
|
||||||
@@ -632,6 +1020,123 @@ const docTemplate = `{
|
|||||||
"type": "string"
|
"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": {
|
"/users": {
|
||||||
"post": {
|
"post": {
|
||||||
"consumes": [
|
"consumes": [
|
||||||
@@ -490,6 +826,55 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"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": {
|
||||||
@@ -563,6 +948,9 @@
|
|||||||
"name": {
|
"name": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
|
"telegram_chat_id": {
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
"telegram_chat_name": {
|
"telegram_chat_name": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
}
|
}
|
||||||
@@ -621,6 +1009,123 @@
|
|||||||
"type": "string"
|
"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:
|
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:
|
||||||
@@ -47,6 +80,8 @@ definitions:
|
|||||||
properties:
|
properties:
|
||||||
name:
|
name:
|
||||||
type: string
|
type: string
|
||||||
|
telegram_chat_id:
|
||||||
|
type: integer
|
||||||
telegram_chat_name:
|
telegram_chat_name:
|
||||||
type: string
|
type: string
|
||||||
type: object
|
type: object
|
||||||
@@ -85,6 +120,84 @@ definitions:
|
|||||||
username:
|
username:
|
||||||
type: string
|
type: string
|
||||||
type: object
|
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:
|
info:
|
||||||
contact: {}
|
contact: {}
|
||||||
paths:
|
paths:
|
||||||
@@ -243,6 +356,230 @@ paths:
|
|||||||
summary: Обновить семью
|
summary: Обновить семью
|
||||||
tags:
|
tags:
|
||||||
- Families
|
- 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:
|
/users:
|
||||||
post:
|
post:
|
||||||
consumes:
|
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"
|
"FamilyHub/src/domain"
|
||||||
"database/sql"
|
"database/sql"
|
||||||
"errors"
|
"errors"
|
||||||
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"runtime/debug"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
@@ -23,7 +25,7 @@ func (router *FamiliesRouter) RegisterRoutes(r *gin.RouterGroup) {
|
|||||||
families := r.Group("/families")
|
families := r.Group("/families")
|
||||||
{
|
{
|
||||||
families.POST("", router.Create)
|
families.POST("", router.Create)
|
||||||
families.GET("/:id", router.GetByID)
|
families.GET("/:id", router.Read)
|
||||||
families.PATCH("/:id", router.Update)
|
families.PATCH("/:id", router.Update)
|
||||||
families.DELETE("/:id", router.Delete)
|
families.DELETE("/:id", router.Delete)
|
||||||
}
|
}
|
||||||
@@ -58,7 +60,7 @@ func (router *FamiliesRouter) Create(c *gin.Context) {
|
|||||||
c.JSON(http.StatusCreated, resp.ModelToResponse(family))
|
c.JSON(http.StatusCreated, resp.ModelToResponse(family))
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetByID GoDoc
|
// Read GoDoc
|
||||||
// @Summary Получить семью по ID
|
// @Summary Получить семью по ID
|
||||||
// @Description Возвращает семью по ее внутреннему ID
|
// @Description Возвращает семью по ее внутреннему ID
|
||||||
// @Tags Families
|
// @Tags Families
|
||||||
@@ -70,7 +72,7 @@ func (router *FamiliesRouter) Create(c *gin.Context) {
|
|||||||
// @Failure 404 {object} map[string]string "family not found"
|
// @Failure 404 {object} map[string]string "family not found"
|
||||||
// @Failure 500 {object} map[string]string "internal server error"
|
// @Failure 500 {object} map[string]string "internal server error"
|
||||||
// @Router /families/{id} [get]
|
// @Router /families/{id} [get]
|
||||||
func (router *FamiliesRouter) GetByID(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 := strconv.ParseInt(c.Param("id"), 10, 64)
|
||||||
@@ -164,6 +166,14 @@ func handleFamilyError(c *gin.Context, err error) {
|
|||||||
case errors.Is(err, sql.ErrNoRows):
|
case errors.Is(err, sql.ErrNoRows):
|
||||||
c.JSON(http.StatusNotFound, gin.H{"error": "family not found"})
|
c.JSON(http.StatusNotFound, gin.H{"error": "family not found"})
|
||||||
default:
|
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"})
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "internal server error"})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,6 +19,14 @@ import (
|
|||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func int64Ptr(v int64) *int64 {
|
||||||
|
return &v
|
||||||
|
}
|
||||||
|
|
||||||
|
func stringPtr(v string) *string {
|
||||||
|
return &v
|
||||||
|
}
|
||||||
|
|
||||||
type familyServiceMock struct {
|
type familyServiceMock struct {
|
||||||
createFn func(ctx context.Context, req domain.CreateFamilyRequest) (*domain.Family, error)
|
createFn func(ctx context.Context, req domain.CreateFamilyRequest) (*domain.Family, error)
|
||||||
getByIDFn func(ctx context.Context, id int64) (*domain.Family, error)
|
getByIDFn func(ctx context.Context, id int64) (*domain.Family, error)
|
||||||
@@ -68,8 +76,8 @@ func sampleFamily() *domain.Family {
|
|||||||
ID: 7,
|
ID: 7,
|
||||||
Name: "Belan",
|
Name: "Belan",
|
||||||
OwnerID: 10,
|
OwnerID: 10,
|
||||||
TelegramChatID: 12345,
|
TelegramChatID: int64Ptr(12345),
|
||||||
TelegramChatName: "Family Chat",
|
TelegramChatName: stringPtr("Family Chat"),
|
||||||
CreatedAt: time.Date(2026, time.January, 21, 10, 0, 0, 0, time.UTC),
|
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),
|
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()
|
expected := sampleFamily()
|
||||||
r := setupFamiliesRouter(&familyServiceMock{createFn: func(ctx context.Context, req domain.CreateFamilyRequest) (*domain.Family, error) {
|
r := setupFamiliesRouter(&familyServiceMock{createFn: func(ctx context.Context, req domain.CreateFamilyRequest) (*domain.Family, error) {
|
||||||
assert.Equal(t, "Belan", req.Name)
|
assert.Equal(t, "Belan", req.Name)
|
||||||
|
assert.Nil(t, req.TelegramChatID)
|
||||||
|
assert.Nil(t, req.TelegramChatName)
|
||||||
return expected, nil
|
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")
|
req.Header.Set("Content-Type", "application/json")
|
||||||
w := httptest.NewRecorder()
|
w := httptest.NewRecorder()
|
||||||
|
|
||||||
@@ -233,7 +243,8 @@ func TestFamiliesRouter_Update(t *testing.T) {
|
|||||||
assert.Equal(t, int64(7), id)
|
assert.Equal(t, int64(7), id)
|
||||||
require.NotNil(t, req.Name)
|
require.NotNil(t, req.Name)
|
||||||
assert.Equal(t, updatedName, *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
|
return expected, nil
|
||||||
}})
|
}})
|
||||||
req := httptest.NewRequest(http.MethodPatch, "/api/v1/families/7", bytes.NewBufferString(`{"name":"`+updatedName+`","telegram_chat_name":"Updated"}`))
|
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 {
|
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 {
|
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) {
|
func (router *ReceiptRouter) AddReceipt(context_ *gin.Context) {
|
||||||
var req domain.AddReceiptRequest
|
var req domain.AddReceiptRequest
|
||||||
if err := context_.ShouldBindJSON(&req); err != nil {
|
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)
|
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
receipt, err := router.service.GetReceipt(ctx, isoDate, req.Number)
|
req.Date = isoDate
|
||||||
|
|
||||||
|
receipt, err := router.service.GetReceipt(ctx, req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
context_.JSON(400, gin.H{"error": err.Error()})
|
context_.JSON(400, gin.H{"error": err.Error()})
|
||||||
log.Printf("API error, %s", err.Error())
|
log.Printf("API error, %s", err.Error())
|
||||||
@@ -54,9 +67,10 @@ func (router *ReceiptRouter) AddReceipt(context_ *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
resp := domain.AddReceiptResponse{
|
resp := domain.AddReceiptResponse{
|
||||||
ID: 1,
|
ID: int32(receipt.ID),
|
||||||
Number: receipt.ReceiptNumber,
|
Number: receipt.ReceiptNumber,
|
||||||
Date: receipt.IssuedAt,
|
Date: receipt.IssuedAt,
|
||||||
|
TransactionID: receipt.TransactionID,
|
||||||
}
|
}
|
||||||
context_.JSON(http.StatusOK, resp)
|
context_.JSON(http.StatusOK, resp)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,12 +18,12 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type receiptServiceMock struct {
|
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 {
|
if m.getReceiptFn != nil {
|
||||||
return m.getReceiptFn(ctx, date, number)
|
return m.getReceiptFn(ctx, req)
|
||||||
}
|
}
|
||||||
return nil, errors.New("mock is not configured")
|
return nil, errors.New("mock is not configured")
|
||||||
}
|
}
|
||||||
@@ -60,9 +60,9 @@ func TestReceiptRouter_AddReceipt(t *testing.T) {
|
|||||||
{
|
{
|
||||||
name: "bad request on service error",
|
name: "bad request on service error",
|
||||||
body: `{"number":"` + validNumber + `","date":"` + validDate + `"}`,
|
body: `{"number":"` + validNumber + `","date":"` + validDate + `"}`,
|
||||||
mock: &receiptServiceMock{getReceiptFn: func(ctx context.Context, date string, number string) (*domain.Receipt, error) {
|
mock: &receiptServiceMock{getReceiptFn: func(ctx context.Context, req domain.AddReceiptRequest) (*domain.Receipt, error) {
|
||||||
assert.Equal(t, expectedDate, date)
|
assert.Equal(t, expectedDate, req.Date)
|
||||||
assert.Equal(t, validNumber, number)
|
assert.Equal(t, validNumber, req.Number)
|
||||||
return nil, errors.New("receipt not found")
|
return nil, errors.New("receipt not found")
|
||||||
}},
|
}},
|
||||||
expectedStatus: http.StatusBadRequest,
|
expectedStatus: http.StatusBadRequest,
|
||||||
@@ -71,10 +71,10 @@ func TestReceiptRouter_AddReceipt(t *testing.T) {
|
|||||||
{
|
{
|
||||||
name: "ok",
|
name: "ok",
|
||||||
body: `{"number":"` + validNumber + `","date":"` + validDate + `"}`,
|
body: `{"number":"` + validNumber + `","date":"` + validDate + `"}`,
|
||||||
mock: &receiptServiceMock{getReceiptFn: func(ctx context.Context, date string, number string) (*domain.Receipt, error) {
|
mock: &receiptServiceMock{getReceiptFn: func(ctx context.Context, req domain.AddReceiptRequest) (*domain.Receipt, error) {
|
||||||
assert.Equal(t, expectedDate, date)
|
assert.Equal(t, expectedDate, req.Date)
|
||||||
assert.Equal(t, validNumber, number)
|
assert.Equal(t, validNumber, req.Number)
|
||||||
return &domain.Receipt{ReceiptNumber: validNumber, IssuedAt: now}, nil
|
return &domain.Receipt{ID: 7, ReceiptNumber: validNumber, IssuedAt: now}, nil
|
||||||
}},
|
}},
|
||||||
expectedStatus: http.StatusOK,
|
expectedStatus: http.StatusOK,
|
||||||
expectedContains: validNumber,
|
expectedContains: validNumber,
|
||||||
@@ -108,7 +108,7 @@ func TestReceiptRouter_AddReceipt(t *testing.T) {
|
|||||||
}
|
}
|
||||||
err := json.Unmarshal(w.Body.Bytes(), &resp)
|
err := json.Unmarshal(w.Body.Bytes(), &resp)
|
||||||
require.NoError(t, err)
|
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, validNumber, resp.Number)
|
||||||
assert.Equal(t, now, resp.Date)
|
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) {
|
func (router *UsersRouter) RegisterRoutes(r *gin.RouterGroup) {
|
||||||
users := r.Group("/users")
|
users := r.Group("/users")
|
||||||
{
|
{
|
||||||
users.POST("", router.CreateUser)
|
users.POST("", router.Create)
|
||||||
users.GET("/:id", router.GetByID)
|
users.GET("/:id", router.Read)
|
||||||
users.GET("/by-telegram/:telegramId", router.GetByTelegramID)
|
|
||||||
users.PATCH("/:id", router.Update)
|
users.PATCH("/:id", router.Update)
|
||||||
users.DELETE("/:id", router.Delete)
|
users.DELETE("/:id", router.Delete)
|
||||||
|
users.GET("/by-telegram/:telegramId", router.GetByTelegramID)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// CreateUser GoDoc
|
// Create GoDoc
|
||||||
// @Summary Создать пользователя
|
// @Summary Создать пользователя
|
||||||
// @Tags Users
|
// @Tags Users
|
||||||
// @Accept json
|
// @Accept json
|
||||||
@@ -39,7 +39,7 @@ func (router *UsersRouter) RegisterRoutes(r *gin.RouterGroup) {
|
|||||||
// @Failure 400 {object} domain.UserErrorResponse
|
// @Failure 400 {object} domain.UserErrorResponse
|
||||||
// @Failure 500 {object} domain.UserErrorResponse
|
// @Failure 500 {object} domain.UserErrorResponse
|
||||||
// @Router /users [post]
|
// @Router /users [post]
|
||||||
func (router *UsersRouter) CreateUser(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
|
||||||
|
|
||||||
@@ -57,7 +57,7 @@ func (router *UsersRouter) CreateUser(c *gin.Context) {
|
|||||||
c.JSON(http.StatusCreated, resp.ModelToResponse(user))
|
c.JSON(http.StatusCreated, resp.ModelToResponse(user))
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetByID GoDoc
|
// Read GoDoc
|
||||||
// @Summary Получить пользователя по ID
|
// @Summary Получить пользователя по ID
|
||||||
// @Description Возвращает пользователя по его внутреннему ID
|
// @Description Возвращает пользователя по его внутреннему ID
|
||||||
// @Tags Users
|
// @Tags Users
|
||||||
@@ -69,7 +69,7 @@ func (router *UsersRouter) CreateUser(c *gin.Context) {
|
|||||||
// @Failure 404 {object} domain.UserErrorResponse "user not found"
|
// @Failure 404 {object} domain.UserErrorResponse "user not found"
|
||||||
// @Failure 500 {object} domain.UserErrorResponse "internal server error"
|
// @Failure 500 {object} domain.UserErrorResponse "internal server error"
|
||||||
// @Router /users/{id} [get]
|
// @Router /users/{id} [get]
|
||||||
func (router *UsersRouter) GetByID(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 := strconv.ParseInt(c.Param("id"), 10, 64)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import (
|
|||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
@@ -36,8 +37,10 @@ func NewServer(cfg config.Config) *Server {
|
|||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
gin.SetMode(gin.ReleaseMode)
|
//gin.SetMode(gin.ReleaseMode)
|
||||||
router := gin.Default()
|
router := gin.New()
|
||||||
|
router.Use(gin.Logger())
|
||||||
|
router.Use(gin.RecoveryWithWriter(os.Stderr))
|
||||||
if cfg.OpenAPIEnabled {
|
if cfg.OpenAPIEnabled {
|
||||||
openAPIEndpoint := cfg.OpenAPIEndpoint
|
openAPIEndpoint := cfg.OpenAPIEndpoint
|
||||||
if openAPIEndpoint == "" {
|
if openAPIEndpoint == "" {
|
||||||
@@ -84,11 +87,17 @@ func NewServer(cfg config.Config) *Server {
|
|||||||
|
|
||||||
apiV1 := router.Group("/api/v1")
|
apiV1 := router.Group("/api/v1")
|
||||||
|
|
||||||
|
transactionRepo := repositories.NewTransactionsSQLRepository(dbConn)
|
||||||
|
|
||||||
receiptRepo := repositories.NewReceiptsSQLRepository(dbConn)
|
receiptRepo := repositories.NewReceiptsSQLRepository(dbConn)
|
||||||
receiptService_ := receiptService.NewReceiptService(receiptRepo)
|
receiptService_ := receiptService.NewReceiptService(receiptRepo, transactionRepo)
|
||||||
receiptRouter := routers.NewReceiptRouter(receiptService_)
|
receiptRouter := routers.NewReceiptRouter(receiptService_)
|
||||||
receiptRouter.RegisterRoutes(apiV1)
|
receiptRouter.RegisterRoutes(apiV1)
|
||||||
|
|
||||||
|
transactionService := services.NewTransactionService(transactionRepo)
|
||||||
|
transactionRouter := routers.NewTransactionsRouter(transactionService)
|
||||||
|
transactionRouter.RegisterRoutes(apiV1)
|
||||||
|
|
||||||
usersRepo := repositories.NewUsersSQLRepository(dbConn)
|
usersRepo := repositories.NewUsersSQLRepository(dbConn)
|
||||||
usersService := services.NewUserService(usersRepo)
|
usersService := services.NewUserService(usersRepo)
|
||||||
usersRouter := routers.NewUsersRouter(usersService)
|
usersRouter := routers.NewUsersRouter(usersService)
|
||||||
|
|||||||
@@ -61,7 +61,7 @@ func (s *familyService) Update(ctx context.Context, id int64, req domain.UpdateF
|
|||||||
ID: id,
|
ID: id,
|
||||||
Name: *req.Name,
|
Name: *req.Name,
|
||||||
OwnerID: existing.OwnerID,
|
OwnerID: existing.OwnerID,
|
||||||
TelegramChatID: existing.TelegramChatID,
|
TelegramChatID: req.TelegramChatID,
|
||||||
TelegramChatName: req.TelegramChatName,
|
TelegramChatName: req.TelegramChatName,
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
return nil, err
|
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)
|
||||||
|
}
|
||||||
@@ -76,12 +76,13 @@ func (h *Handler) handleCreateFamilyName(msg *tgbotapi.Message) {
|
|||||||
if strings.TrimSpace(chatName) == "" {
|
if strings.TrimSpace(chatName) == "" {
|
||||||
chatName = familyName
|
chatName = familyName
|
||||||
}
|
}
|
||||||
|
chatID := msg.Chat.ID
|
||||||
|
|
||||||
err = h.receiptApi.CreateFamily(ctx, domain.CreateFamilyRequest{
|
err = h.receiptApi.CreateFamily(ctx, domain.CreateFamilyRequest{
|
||||||
Name: familyName,
|
Name: familyName,
|
||||||
OwnerID: user.ID,
|
OwnerID: user.ID,
|
||||||
TelegramChatID: msg.Chat.ID,
|
TelegramChatID: &chatID,
|
||||||
TelegramChatName: chatName,
|
TelegramChatName: &chatName,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("failed to create family in api: %v", err)
|
log.Printf("failed to create family in api: %v", err)
|
||||||
|
|||||||
@@ -3,5 +3,6 @@ package domain
|
|||||||
type AuthRequest struct {
|
type AuthRequest struct {
|
||||||
TelegramId *string `json:"telegram_id"`
|
TelegramId *string `json:"telegram_id"`
|
||||||
OTP *int64 `json:"otp"`
|
OTP *int64 `json:"otp"`
|
||||||
InitData *string `json:"init_data"`
|
|
||||||
|
InitData *string `json:"init_data"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,8 +6,8 @@ type Family struct {
|
|||||||
ID int64
|
ID int64
|
||||||
Name string
|
Name string
|
||||||
OwnerID int64
|
OwnerID int64
|
||||||
TelegramChatID int64
|
TelegramChatID *int64
|
||||||
TelegramChatName string
|
TelegramChatName *string
|
||||||
CreatedAt time.Time
|
CreatedAt time.Time
|
||||||
UpdatedAt time.Time
|
UpdatedAt time.Time
|
||||||
}
|
}
|
||||||
@@ -29,37 +29,27 @@ type FamilyMember struct {
|
|||||||
JoinedAt time.Time
|
JoinedAt time.Time
|
||||||
}
|
}
|
||||||
|
|
||||||
type FamilyThread struct {
|
|
||||||
ID int64
|
|
||||||
FamilyID int64
|
|
||||||
Type string
|
|
||||||
Title string
|
|
||||||
TelegramTopicID int64
|
|
||||||
IsSystem bool
|
|
||||||
CreatedBy int64
|
|
||||||
CreatedAt time.Time
|
|
||||||
}
|
|
||||||
|
|
||||||
type CreateFamilyRequest struct {
|
type CreateFamilyRequest struct {
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
OwnerID int64 `json:"owner_id"`
|
OwnerID int64 `json:"owner_id"`
|
||||||
TelegramChatID int64 `json:"telegram_chat_id"`
|
TelegramChatID *int64 `json:"telegram_chat_id"`
|
||||||
TelegramChatName string `json:"telegram_chat_name"`
|
TelegramChatName *string `json:"telegram_chat_name"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type UpdateFamilyRequest struct {
|
type UpdateFamilyRequest struct {
|
||||||
Name *string `json:"name"`
|
Name *string `json:"name"`
|
||||||
TelegramChatName string `json:"telegram_chat_name"`
|
TelegramChatID *int64 `json:"telegram_chat_id"`
|
||||||
|
TelegramChatName *string `json:"telegram_chat_name"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type FamilyResponse struct {
|
type FamilyResponse struct {
|
||||||
ID int64 `json:"id"`
|
ID int64 `json:"id"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
OwnerID int64 `json:"owner_id"`
|
OwnerID int64 `json:"owner_id"`
|
||||||
TelegramChatID int64 `json:"telegram_chat_id"`
|
TelegramChatID *int64 `json:"telegram_chat_id"`
|
||||||
TelegramChatName string `json:"telegram_chat_name"`
|
TelegramChatName *string `json:"telegram_chat_name"`
|
||||||
CreatedAt string `json:"created_at"`
|
CreatedAt string `json:"created_at"`
|
||||||
UpdatedAt string `json:"updated_at"`
|
UpdatedAt string `json:"updated_at"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (response *FamilyResponse) ModelToResponse(f *Family) FamilyResponse {
|
func (response *FamilyResponse) ModelToResponse(f *Family) FamilyResponse {
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ package domain
|
|||||||
import "time"
|
import "time"
|
||||||
|
|
||||||
type Position struct {
|
type Position struct {
|
||||||
|
ID int64 `json:"id"`
|
||||||
|
ReceiptID int64 `json:"receipt_id"`
|
||||||
SectionNumber string `json:"section_number"`
|
SectionNumber string `json:"section_number"`
|
||||||
GTINCode string `json:"gtin_code"`
|
GTINCode string `json:"gtin_code"`
|
||||||
Tag string `json:"tag"`
|
Tag string `json:"tag"`
|
||||||
@@ -25,6 +27,7 @@ type Position struct {
|
|||||||
|
|
||||||
type Receipt struct {
|
type Receipt struct {
|
||||||
ID int `json:"id"`
|
ID int `json:"id"`
|
||||||
|
TransactionID *int64 `json:"transaction_id"`
|
||||||
Status int `json:"STATUS"`
|
Status int `json:"STATUS"`
|
||||||
AnotherAmount float64 `json:"another_amount"`
|
AnotherAmount float64 `json:"another_amount"`
|
||||||
CashAmount float64 `json:"cash_amount"`
|
CashAmount float64 `json:"cash_amount"`
|
||||||
@@ -64,12 +67,18 @@ type Receipt struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type AddReceiptRequest struct {
|
type AddReceiptRequest struct {
|
||||||
Number string `json:"number" binding:"required,min=24,max=24"`
|
Number string `json:"number" binding:"required,min=24,max=24"`
|
||||||
Date string `json:"date" binding:"required"`
|
Date string `json:"date" binding:"required"`
|
||||||
|
FamilyID *int64 `json:"family_id"`
|
||||||
|
CreatedBy *int64 `json:"created_by"`
|
||||||
|
Type *string `json:"type"`
|
||||||
|
Category *string `json:"category"`
|
||||||
|
Description *string `json:"description"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type AddReceiptResponse struct {
|
type AddReceiptResponse struct {
|
||||||
ID int32 `json:"id"`
|
ID int32 `json:"id"`
|
||||||
Number string `json:"number"`
|
Number string `json:"number"`
|
||||||
Date time.Time `json:"date"`
|
Date time.Time `json:"date"`
|
||||||
|
TransactionID *int64 `json:"transaction_id,omitempty"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,48 @@
|
|||||||
|
package domain
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
|
type Transaction struct {
|
||||||
|
ID int64
|
||||||
|
FamilyID int64
|
||||||
|
Description *string
|
||||||
|
Type string
|
||||||
|
DateTime time.Time
|
||||||
|
Category string
|
||||||
|
Amount float64
|
||||||
|
CreatedAt time.Time
|
||||||
|
CreatedBy int64
|
||||||
|
ReceiptID *int64
|
||||||
|
}
|
||||||
|
|
||||||
|
type CreateTransactionRequest struct {
|
||||||
|
FamilyID int64
|
||||||
|
Description *string
|
||||||
|
Type string
|
||||||
|
DateTime time.Time
|
||||||
|
Category string
|
||||||
|
Amount float64
|
||||||
|
CreatedBy int64
|
||||||
|
ReceiptID *int64
|
||||||
|
}
|
||||||
|
|
||||||
|
type UpdateTransactionRequest struct {
|
||||||
|
Description *string
|
||||||
|
Type *string
|
||||||
|
DateTime *time.Time
|
||||||
|
Category *string
|
||||||
|
Amount *float64
|
||||||
|
ReceiptID *int64
|
||||||
|
DetachReceipt bool
|
||||||
|
}
|
||||||
|
|
||||||
|
type TransactionListFilter struct {
|
||||||
|
FamilyID *int64
|
||||||
|
CreatedBy *int64
|
||||||
|
Type *string
|
||||||
|
Category *string
|
||||||
|
DateFrom *time.Time
|
||||||
|
DateTo *time.Time
|
||||||
|
Limit int
|
||||||
|
Offset int
|
||||||
|
}
|
||||||
@@ -13,15 +13,20 @@ import (
|
|||||||
"log"
|
"log"
|
||||||
"mime/multipart"
|
"mime/multipart"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
type ReceiptService struct {
|
type ReceiptService struct {
|
||||||
client *http.Client
|
client *http.Client
|
||||||
repo repositories.ReceiptsRepository
|
repo repositories.ReceiptsRepository
|
||||||
|
transactionRepo repositories.TransactionRepository
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewReceiptService(repo repositories.ReceiptsRepository) *ReceiptService {
|
func NewReceiptService(
|
||||||
|
repo repositories.ReceiptsRepository,
|
||||||
|
transactionRepo repositories.TransactionRepository,
|
||||||
|
) *ReceiptService {
|
||||||
return &ReceiptService{
|
return &ReceiptService{
|
||||||
client: &http.Client{
|
client: &http.Client{
|
||||||
Timeout: 60 * time.Second,
|
Timeout: 60 * time.Second,
|
||||||
@@ -31,20 +36,20 @@ func NewReceiptService(repo repositories.ReceiptsRepository) *ReceiptService {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
repo: repo,
|
repo: repo,
|
||||||
|
transactionRepo: transactionRepo,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *ReceiptService) GetReceipt(
|
func (s *ReceiptService) GetReceipt(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
date string,
|
req domain.AddReceiptRequest,
|
||||||
number string,
|
|
||||||
) (*domain.Receipt, error) {
|
) (*domain.Receipt, error) {
|
||||||
url := "https://ch.info-center.by/ajax/check1.php"
|
url := "https://ch.info-center.by/ajax/check1.php"
|
||||||
var receipt domain.Receipt
|
var receipt domain.Receipt
|
||||||
|
|
||||||
body, contentType := buildMultipartBody(date, number)
|
body, contentType := buildMultipartBody(req.Date, req.Number)
|
||||||
req, err := http.NewRequestWithContext(
|
httpReq, err := http.NewRequestWithContext(
|
||||||
ctx,
|
ctx,
|
||||||
http.MethodPost,
|
http.MethodPost,
|
||||||
url,
|
url,
|
||||||
@@ -55,9 +60,9 @@ func (s *ReceiptService) GetReceipt(
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
req.Header.Set("Content-Type", contentType)
|
httpReq.Header.Set("Content-Type", contentType)
|
||||||
|
|
||||||
resp, err := s.client.Do(req)
|
resp, err := s.client.Do(httpReq)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Println(err.Error())
|
log.Println(err.Error())
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -119,9 +124,23 @@ func (s *ReceiptService) GetReceipt(
|
|||||||
p.Surcharge, _ = utils.ParseFloat(p.SurchargeRaw)
|
p.Surcharge, _ = utils.ParseFloat(p.SurchargeRaw)
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, err := s.repo.Create(ctx, &receipt); err != nil {
|
receiptID, err := s.repo.Create(ctx, &receipt)
|
||||||
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
receipt.ID = int(receiptID)
|
||||||
|
|
||||||
|
if s.shouldCreateTransaction(req) {
|
||||||
|
transaction, err := s.createTransactionForReceipt(ctx, &receipt, req, receiptID)
|
||||||
|
if err != nil {
|
||||||
|
if rollbackErr := s.repo.Delete(ctx, receiptID); rollbackErr != nil {
|
||||||
|
log.Printf("failed to rollback receipt %d after transaction error: %v", receiptID, rollbackErr)
|
||||||
|
}
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
receipt.TransactionID = &transaction.ID
|
||||||
|
}
|
||||||
|
|
||||||
return &receipt, nil
|
return &receipt, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -150,3 +169,61 @@ func parsePositions(raw string) ([]domain.Position, error) {
|
|||||||
|
|
||||||
return positions, nil
|
return positions, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *ReceiptService) shouldCreateTransaction(req domain.AddReceiptRequest) bool {
|
||||||
|
return s.transactionRepo != nil && req.FamilyID != nil && req.CreatedBy != nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ReceiptService) createTransactionForReceipt(
|
||||||
|
ctx context.Context,
|
||||||
|
receipt *domain.Receipt,
|
||||||
|
req domain.AddReceiptRequest,
|
||||||
|
receiptID int64,
|
||||||
|
) (*domain.Transaction, error) {
|
||||||
|
transactionType := "expense"
|
||||||
|
if req.Type != nil && strings.TrimSpace(*req.Type) != "" {
|
||||||
|
transactionType = strings.TrimSpace(*req.Type)
|
||||||
|
}
|
||||||
|
|
||||||
|
category := "receipt"
|
||||||
|
if req.Category != nil && strings.TrimSpace(*req.Category) != "" {
|
||||||
|
category = strings.TrimSpace(*req.Category)
|
||||||
|
}
|
||||||
|
|
||||||
|
description := buildReceiptTransactionDescription(receipt, req.Description)
|
||||||
|
|
||||||
|
transaction := &domain.Transaction{
|
||||||
|
FamilyID: *req.FamilyID,
|
||||||
|
Description: description,
|
||||||
|
Type: transactionType,
|
||||||
|
DateTime: receipt.IssuedAt,
|
||||||
|
Category: category,
|
||||||
|
Amount: receipt.TotalAmount,
|
||||||
|
CreatedBy: *req.CreatedBy,
|
||||||
|
ReceiptID: &receiptID,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := s.transactionRepo.Create(ctx, transaction); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return transaction, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildReceiptTransactionDescription(receipt *domain.Receipt, explicit *string) *string {
|
||||||
|
if explicit != nil && strings.TrimSpace(*explicit) != "" {
|
||||||
|
value := strings.TrimSpace(*explicit)
|
||||||
|
return &value
|
||||||
|
}
|
||||||
|
|
||||||
|
if name := strings.TrimSpace(receipt.NameSPD); name != "" {
|
||||||
|
return &name
|
||||||
|
}
|
||||||
|
|
||||||
|
if number := strings.TrimSpace(receipt.ReceiptNumber); number != "" {
|
||||||
|
value := fmt.Sprintf("Receipt %s", number)
|
||||||
|
return &value
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -93,7 +93,6 @@ func (r *FamilySQLRepository) Update(ctx context.Context, family *domain.Family)
|
|||||||
family.Name,
|
family.Name,
|
||||||
family.TelegramChatID,
|
family.TelegramChatID,
|
||||||
family.TelegramChatName,
|
family.TelegramChatName,
|
||||||
family.UpdatedAt,
|
|
||||||
family.ID,
|
family.ID,
|
||||||
).Scan(&family.UpdatedAt)
|
).Scan(&family.UpdatedAt)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ func (r *ReceiptsSQLRepository) Create(ctx context.Context, receipt *domain.Rece
|
|||||||
}
|
}
|
||||||
res, err := tx.ExecContext(ctx, `
|
res, err := tx.ExecContext(ctx, `
|
||||||
INSERT INTO receipts (
|
INSERT INTO receipts (
|
||||||
receipt_number, ui, status, issued_at,
|
transaction_id, receipt_number, ui, status, issued_at,
|
||||||
total_amount, payment_amount, cash_amount,
|
total_amount, payment_amount, cash_amount,
|
||||||
another_amount, clearing_amount, margin,
|
another_amount, clearing_amount, margin,
|
||||||
currency, payment_type,
|
currency, payment_type,
|
||||||
@@ -46,8 +46,9 @@ func (r *ReceiptsSQLRepository) Create(ctx context.Context, receipt *domain.Rece
|
|||||||
kod_soato, oblast_soato, rayon_soato, selsovet_soato,
|
kod_soato, oblast_soato, rayon_soato, selsovet_soato,
|
||||||
doc_num, skno_number, unp,
|
doc_num, skno_number, unp,
|
||||||
success
|
success
|
||||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||||
`,
|
`,
|
||||||
|
receipt.TransactionID,
|
||||||
receipt.ReceiptNumber,
|
receipt.ReceiptNumber,
|
||||||
receipt.UI,
|
receipt.UI,
|
||||||
receipt.Status,
|
receipt.Status,
|
||||||
@@ -144,6 +145,7 @@ func (r *ReceiptsSQLRepository) GetByID(ctx context.Context, id int64) (*domain.
|
|||||||
err := r.db.QueryRowContext(ctx, `
|
err := r.db.QueryRowContext(ctx, `
|
||||||
SELECT
|
SELECT
|
||||||
id,
|
id,
|
||||||
|
transaction_id,
|
||||||
receipt_number, ui, status, issued_at,
|
receipt_number, ui, status, issued_at,
|
||||||
total_amount, payment_amount, cash_amount,
|
total_amount, payment_amount, cash_amount,
|
||||||
another_amount, clearing_amount, margin,
|
another_amount, clearing_amount, margin,
|
||||||
@@ -158,6 +160,7 @@ func (r *ReceiptsSQLRepository) GetByID(ctx context.Context, id int64) (*domain.
|
|||||||
WHERE id = ?
|
WHERE id = ?
|
||||||
`, id).Scan(
|
`, id).Scan(
|
||||||
&receipt.ID,
|
&receipt.ID,
|
||||||
|
&receipt.TransactionID,
|
||||||
&receipt.ReceiptNumber,
|
&receipt.ReceiptNumber,
|
||||||
&receipt.UI,
|
&receipt.UI,
|
||||||
&receipt.Status,
|
&receipt.Status,
|
||||||
@@ -205,6 +208,7 @@ func (r *ReceiptsSQLRepository) GetByID(ctx context.Context, id int64) (*domain.
|
|||||||
|
|
||||||
rows, err := r.db.QueryContext(ctx, `
|
rows, err := r.db.QueryContext(ctx, `
|
||||||
SELECT
|
SELECT
|
||||||
|
id, receipt_id,
|
||||||
section_number, gtin_code, product_name,
|
section_number, gtin_code, product_name,
|
||||||
product_count, amount,
|
product_count, amount,
|
||||||
discount, surcharge,
|
discount, surcharge,
|
||||||
@@ -219,6 +223,8 @@ func (r *ReceiptsSQLRepository) GetByID(ctx context.Context, id int64) (*domain.
|
|||||||
for rows.Next() {
|
for rows.Next() {
|
||||||
var p domain.Position
|
var p domain.Position
|
||||||
if err := rows.Scan(
|
if err := rows.Scan(
|
||||||
|
&p.ID,
|
||||||
|
&p.ReceiptID,
|
||||||
&p.SectionNumber,
|
&p.SectionNumber,
|
||||||
&p.GTINCode,
|
&p.GTINCode,
|
||||||
&p.ProductName,
|
&p.ProductName,
|
||||||
@@ -241,7 +247,7 @@ func (r *ReceiptsSQLRepository) GetByID(ctx context.Context, id int64) (*domain.
|
|||||||
func (r *ReceiptsSQLRepository) GetAll(ctx context.Context, limit, offset int) ([]*domain.Receipt, error) {
|
func (r *ReceiptsSQLRepository) GetAll(ctx context.Context, limit, offset int) ([]*domain.Receipt, error) {
|
||||||
|
|
||||||
rows, err := r.db.QueryContext(ctx, `
|
rows, err := r.db.QueryContext(ctx, `
|
||||||
SELECT id, receipt_number, issued_at, total_amount, currency
|
SELECT id, transaction_id, receipt_number, issued_at, total_amount, currency
|
||||||
FROM receipts
|
FROM receipts
|
||||||
ORDER BY issued_at DESC
|
ORDER BY issued_at DESC
|
||||||
LIMIT ? OFFSET ?
|
LIMIT ? OFFSET ?
|
||||||
@@ -257,6 +263,7 @@ func (r *ReceiptsSQLRepository) GetAll(ctx context.Context, limit, offset int) (
|
|||||||
var rct domain.Receipt
|
var rct domain.Receipt
|
||||||
if err := rows.Scan(
|
if err := rows.Scan(
|
||||||
&rct.ID,
|
&rct.ID,
|
||||||
|
&rct.TransactionID,
|
||||||
&rct.ReceiptNumber,
|
&rct.ReceiptNumber,
|
||||||
&rct.IssuedAt,
|
&rct.IssuedAt,
|
||||||
&rct.TotalAmount,
|
&rct.TotalAmount,
|
||||||
@@ -280,11 +287,13 @@ func (r *ReceiptsSQLRepository) Update(ctx context.Context, receipt *domain.Rece
|
|||||||
|
|
||||||
_, err = tx.ExecContext(ctx, `
|
_, err = tx.ExecContext(ctx, `
|
||||||
UPDATE receipts SET
|
UPDATE receipts SET
|
||||||
|
transaction_id = ?,
|
||||||
issued_at = ?,
|
issued_at = ?,
|
||||||
total_amount = ?,
|
total_amount = ?,
|
||||||
currency = ?
|
currency = ?
|
||||||
WHERE id = ?
|
WHERE id = ?
|
||||||
`,
|
`,
|
||||||
|
receipt.TransactionID,
|
||||||
receipt.IssuedAt,
|
receipt.IssuedAt,
|
||||||
receipt.TotalAmount,
|
receipt.TotalAmount,
|
||||||
receipt.Currency,
|
receipt.Currency,
|
||||||
|
|||||||
@@ -0,0 +1,279 @@
|
|||||||
|
package repositories
|
||||||
|
|
||||||
|
import (
|
||||||
|
"FamilyHub/src/domain"
|
||||||
|
"context"
|
||||||
|
"database/sql"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
var ErrReceiptNotFound = errors.New("receipt not found")
|
||||||
|
|
||||||
|
type TransactionRepository interface {
|
||||||
|
Create(ctx context.Context, transaction *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, transaction *domain.Transaction, syncReceipt bool) error
|
||||||
|
Delete(ctx context.Context, id int64) error
|
||||||
|
}
|
||||||
|
|
||||||
|
type TransactionsSQLRepository struct {
|
||||||
|
db *sql.DB
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewTransactionsSQLRepository(db *sql.DB) *TransactionsSQLRepository {
|
||||||
|
return &TransactionsSQLRepository{db: db}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *TransactionsSQLRepository) Create(ctx context.Context, transaction *domain.Transaction) error {
|
||||||
|
tx, err := r.db.BeginTx(ctx, nil)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer tx.Rollback()
|
||||||
|
|
||||||
|
query := `
|
||||||
|
INSERT INTO transactions
|
||||||
|
(family_id, description, type, datetime, category, amount, created_by)
|
||||||
|
VALUES ($1, $2, $3, $4, $5, $6, $7)
|
||||||
|
RETURNING id, created_at
|
||||||
|
`
|
||||||
|
|
||||||
|
if err := tx.QueryRowContext(
|
||||||
|
ctx,
|
||||||
|
query,
|
||||||
|
transaction.FamilyID,
|
||||||
|
transaction.Description,
|
||||||
|
transaction.Type,
|
||||||
|
transaction.DateTime,
|
||||||
|
transaction.Category,
|
||||||
|
transaction.Amount,
|
||||||
|
transaction.CreatedBy,
|
||||||
|
).Scan(&transaction.ID, &transaction.CreatedAt); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := r.rebindReceipt(ctx, tx, transaction.ID, transaction.ReceiptID); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return tx.Commit()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *TransactionsSQLRepository) GetByID(ctx context.Context, id int64) (*domain.Transaction, error) {
|
||||||
|
query := `
|
||||||
|
SELECT
|
||||||
|
t.id,
|
||||||
|
t.family_id,
|
||||||
|
t.description,
|
||||||
|
t.type,
|
||||||
|
t.datetime,
|
||||||
|
t.category,
|
||||||
|
t.amount,
|
||||||
|
t.created_at,
|
||||||
|
t.created_by,
|
||||||
|
r.id
|
||||||
|
FROM transactions t
|
||||||
|
LEFT JOIN receipts r ON r.transaction_id = t.id
|
||||||
|
WHERE t.id = $1
|
||||||
|
`
|
||||||
|
|
||||||
|
var transaction domain.Transaction
|
||||||
|
err := r.db.QueryRowContext(ctx, query, id).Scan(
|
||||||
|
&transaction.ID,
|
||||||
|
&transaction.FamilyID,
|
||||||
|
&transaction.Description,
|
||||||
|
&transaction.Type,
|
||||||
|
&transaction.DateTime,
|
||||||
|
&transaction.Category,
|
||||||
|
&transaction.Amount,
|
||||||
|
&transaction.CreatedAt,
|
||||||
|
&transaction.CreatedBy,
|
||||||
|
&transaction.ReceiptID,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, sql.ErrNoRows) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &transaction, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *TransactionsSQLRepository) List(ctx context.Context, filter domain.TransactionListFilter) ([]*domain.Transaction, error) {
|
||||||
|
var (
|
||||||
|
whereClauses []string
|
||||||
|
args []any
|
||||||
|
)
|
||||||
|
|
||||||
|
baseQuery := `
|
||||||
|
SELECT
|
||||||
|
t.id,
|
||||||
|
t.family_id,
|
||||||
|
t.description,
|
||||||
|
t.type,
|
||||||
|
t.datetime,
|
||||||
|
t.category,
|
||||||
|
t.amount,
|
||||||
|
t.created_at,
|
||||||
|
t.created_by,
|
||||||
|
r.id
|
||||||
|
FROM transactions t
|
||||||
|
LEFT JOIN receipts r ON r.transaction_id = t.id
|
||||||
|
`
|
||||||
|
|
||||||
|
appendFilter := func(condition string, value any) {
|
||||||
|
args = append(args, value)
|
||||||
|
whereClauses = append(whereClauses, fmt.Sprintf(condition, len(args)))
|
||||||
|
}
|
||||||
|
|
||||||
|
if filter.FamilyID != nil {
|
||||||
|
appendFilter("t.family_id = $%d", *filter.FamilyID)
|
||||||
|
}
|
||||||
|
if filter.CreatedBy != nil {
|
||||||
|
appendFilter("t.created_by = $%d", *filter.CreatedBy)
|
||||||
|
}
|
||||||
|
if filter.Type != nil {
|
||||||
|
appendFilter("t.type = $%d", *filter.Type)
|
||||||
|
}
|
||||||
|
if filter.Category != nil {
|
||||||
|
appendFilter("t.category = $%d", *filter.Category)
|
||||||
|
}
|
||||||
|
if filter.DateFrom != nil {
|
||||||
|
appendFilter("t.datetime >= $%d", *filter.DateFrom)
|
||||||
|
}
|
||||||
|
if filter.DateTo != nil {
|
||||||
|
appendFilter("t.datetime <= $%d", *filter.DateTo)
|
||||||
|
}
|
||||||
|
|
||||||
|
var queryBuilder strings.Builder
|
||||||
|
queryBuilder.WriteString(baseQuery)
|
||||||
|
if len(whereClauses) > 0 {
|
||||||
|
queryBuilder.WriteString(" WHERE ")
|
||||||
|
queryBuilder.WriteString(strings.Join(whereClauses, " AND "))
|
||||||
|
}
|
||||||
|
|
||||||
|
args = append(args, filter.Limit, filter.Offset)
|
||||||
|
queryBuilder.WriteString(fmt.Sprintf(" ORDER BY t.datetime DESC LIMIT $%d OFFSET $%d", len(args)-1, len(args)))
|
||||||
|
|
||||||
|
rows, err := r.db.QueryContext(ctx, queryBuilder.String(), args...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
var transactions []*domain.Transaction
|
||||||
|
for rows.Next() {
|
||||||
|
var transaction domain.Transaction
|
||||||
|
if err := rows.Scan(
|
||||||
|
&transaction.ID,
|
||||||
|
&transaction.FamilyID,
|
||||||
|
&transaction.Description,
|
||||||
|
&transaction.Type,
|
||||||
|
&transaction.DateTime,
|
||||||
|
&transaction.Category,
|
||||||
|
&transaction.Amount,
|
||||||
|
&transaction.CreatedAt,
|
||||||
|
&transaction.CreatedBy,
|
||||||
|
&transaction.ReceiptID,
|
||||||
|
); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
transactions = append(transactions, &transaction)
|
||||||
|
}
|
||||||
|
|
||||||
|
return transactions, rows.Err()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *TransactionsSQLRepository) Update(ctx context.Context, transaction *domain.Transaction, syncReceipt bool) error {
|
||||||
|
tx, err := r.db.BeginTx(ctx, nil)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer tx.Rollback()
|
||||||
|
|
||||||
|
query := `
|
||||||
|
UPDATE transactions SET
|
||||||
|
description = $1,
|
||||||
|
type = $2,
|
||||||
|
datetime = $3,
|
||||||
|
category = $4,
|
||||||
|
amount = $5
|
||||||
|
WHERE id = $6
|
||||||
|
`
|
||||||
|
|
||||||
|
result, err := tx.ExecContext(
|
||||||
|
ctx,
|
||||||
|
query,
|
||||||
|
transaction.Description,
|
||||||
|
transaction.Type,
|
||||||
|
transaction.DateTime,
|
||||||
|
transaction.Category,
|
||||||
|
transaction.Amount,
|
||||||
|
transaction.ID,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
rowsAffected, err := result.RowsAffected()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if rowsAffected == 0 {
|
||||||
|
return sql.ErrNoRows
|
||||||
|
}
|
||||||
|
|
||||||
|
if syncReceipt {
|
||||||
|
if err := r.rebindReceipt(ctx, tx, transaction.ID, transaction.ReceiptID); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return tx.Commit()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *TransactionsSQLRepository) Delete(ctx context.Context, id int64) error {
|
||||||
|
result, err := r.db.ExecContext(ctx, `DELETE FROM transactions WHERE id = $1`, id)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
rowsAffected, err := result.RowsAffected()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if rowsAffected == 0 {
|
||||||
|
return sql.ErrNoRows
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *TransactionsSQLRepository) rebindReceipt(ctx context.Context, tx *sql.Tx, transactionID int64, receiptID *int64) error {
|
||||||
|
if _, err := tx.ExecContext(ctx, `UPDATE receipts SET transaction_id = NULL WHERE transaction_id = $1`, transactionID); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if receiptID == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := tx.ExecContext(ctx, `UPDATE receipts SET transaction_id = $1 WHERE id = $2`, transactionID, *receiptID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
rowsAffected, err := result.RowsAffected()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if rowsAffected == 0 {
|
||||||
|
return ErrReceiptNotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -282,9 +282,9 @@ GET /positions
|
|||||||
---
|
---
|
||||||
|
|
||||||
## 8. Архитектурные решения
|
## 8. Архитектурные решения
|
||||||
|
- Transaction — основная сущность финансов
|
||||||
- Position — основная сущность финансов
|
- Receipt — дополняет транзакцию
|
||||||
- Receipt — агрегат для чеков
|
- Position - позиции из чека
|
||||||
- Категории определяют тип операции
|
- Категории определяют тип операции
|
||||||
- Поддержка multi-tenant через family_id
|
- Поддержка multi-tenant через family_id
|
||||||
|
|
||||||
|
|||||||
+26
-2
@@ -1,5 +1,5 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref } from 'vue';
|
import { computed, onMounted, ref } from 'vue';
|
||||||
import Header from './components/Header.vue';
|
import Header from './components/Header.vue';
|
||||||
import Navigation from './components/Navigation.vue';
|
import Navigation from './components/Navigation.vue';
|
||||||
import BalanceWidget from './components/BalanceWidget.vue';
|
import BalanceWidget from './components/BalanceWidget.vue';
|
||||||
@@ -8,9 +8,16 @@ import RecentActivityWidget from './components/RecentActivityWidget.vue';
|
|||||||
import SwipeCards from './components/SwipeCards.vue';
|
import SwipeCards from './components/SwipeCards.vue';
|
||||||
import FinanceScreen from './components/FinanceScreen.vue';
|
import FinanceScreen from './components/FinanceScreen.vue';
|
||||||
import SettingsScreen from './components/SettingsScreen.vue';
|
import SettingsScreen from './components/SettingsScreen.vue';
|
||||||
|
import { getFamilyById } from './api/families';
|
||||||
|
import { useI18n } from './i18n';
|
||||||
|
|
||||||
const activeScreen = ref('home');
|
const activeScreen = ref('home');
|
||||||
const previousScreen = ref('home');
|
const previousScreen = ref('home');
|
||||||
|
const familyName = ref<string | null>(null);
|
||||||
|
const { t } = useI18n();
|
||||||
|
|
||||||
|
const configuredFamilyId = Number.parseInt(import.meta.env.VITE_FAMILY_ID ?? '1', 10);
|
||||||
|
const headerFamilyName = computed(() => familyName.value?.trim() || t('header.familyName'));
|
||||||
|
|
||||||
function handleNavigate(screen: string) {
|
function handleNavigate(screen: string) {
|
||||||
if (screen === 'settings') {
|
if (screen === 'settings') {
|
||||||
@@ -28,6 +35,23 @@ function handleNavigate(screen: string) {
|
|||||||
|
|
||||||
activeScreen.value = screen;
|
activeScreen.value = screen;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function loadFamily() {
|
||||||
|
if (!Number.isFinite(configuredFamilyId) || configuredFamilyId <= 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const family = await getFamilyById(configuredFamilyId);
|
||||||
|
familyName.value = family.name;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to load family', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
void loadFamily();
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -43,7 +67,7 @@ function handleNavigate(screen: string) {
|
|||||||
|
|
||||||
<div v-else class="min-h-screen bg-[#0A0A0F] dark">
|
<div v-else class="min-h-screen bg-[#0A0A0F] dark">
|
||||||
<div class="mx-auto flex min-h-screen max-w-md flex-col relative">
|
<div class="mx-auto flex min-h-screen max-w-md flex-col relative">
|
||||||
<Header @navigate="handleNavigate" />
|
<Header :family-name="headerFamilyName" @navigate="handleNavigate" />
|
||||||
|
|
||||||
<main class="flex-1 overflow-y-auto px-5 pb-32">
|
<main class="flex-1 overflow-y-auto px-5 pb-32">
|
||||||
<div class="space-y-4">
|
<div class="space-y-4">
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
export interface Family {
|
||||||
|
id: number
|
||||||
|
name: string
|
||||||
|
owner_id: number
|
||||||
|
telegram_chat_id: number | null
|
||||||
|
telegram_chat_name: string | null
|
||||||
|
created_at: string
|
||||||
|
updated_at: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getFamilyById(id: number): Promise<Family> {
|
||||||
|
const response = await fetch(`/api/v1/families/${id}`)
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`Failed to fetch family: ${response.status}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.json() as Promise<Family>
|
||||||
|
}
|
||||||
@@ -6,6 +6,13 @@ import { useI18n } from '@/i18n';
|
|||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
navigate: [screen: string];
|
navigate: [screen: string];
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<{
|
||||||
|
familyName?: string;
|
||||||
|
}>(), {
|
||||||
|
familyName: '',
|
||||||
|
});
|
||||||
|
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
|
|
||||||
const currentHour = ref(new Date().getHours());
|
const currentHour = ref(new Date().getHours());
|
||||||
@@ -51,7 +58,7 @@ onBeforeUnmount(() => {
|
|||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<p class="mb-0.5 text-[11px] font-normal text-zinc-500">{{ greeting }}</p>
|
<p class="mb-0.5 text-[11px] font-normal text-zinc-500">{{ greeting }}</p>
|
||||||
<h1 class="text-[17px] font-semibold tracking-tight text-white">{{ t('header.familyName') }}</h1>
|
<h1 class="text-[17px] font-semibold tracking-tight text-white">{{ props.familyName || t('header.familyName') }}</h1>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
|
|||||||
Vendored
+9
@@ -0,0 +1,9 @@
|
|||||||
|
/// <reference types="vite/client" />
|
||||||
|
|
||||||
|
interface ImportMetaEnv {
|
||||||
|
readonly VITE_FAMILY_ID?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ImportMeta {
|
||||||
|
readonly env: ImportMetaEnv
|
||||||
|
}
|
||||||
@@ -11,6 +11,14 @@ export default defineConfig({
|
|||||||
vue(),
|
vue(),
|
||||||
tailwindcss(),
|
tailwindcss(),
|
||||||
],
|
],
|
||||||
|
server: {
|
||||||
|
proxy: {
|
||||||
|
'/api': {
|
||||||
|
target: 'http://localhost:8000',
|
||||||
|
changeOrigin: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
resolve: {
|
resolve: {
|
||||||
alias: {
|
alias: {
|
||||||
// Alias @ to the src directory
|
// Alias @ to the src directory
|
||||||
|
|||||||
Reference in New Issue
Block a user