Restructured project

- backend moved to backend directory
- added and initialized frontend with vue
- moved infrastructure files to infra directory
This commit is contained in:
2026-04-01 22:27:26 +03:00
parent 48ef7217eb
commit 9d845c8899
96 changed files with 1591 additions and 118 deletions
+655
View File
@@ -0,0 +1,655 @@
// Package docs Code generated by swaggo/swag. DO NOT EDIT
package docs
import "github.com/swaggo/swag"
const docTemplate = `{
"schemes": {{ marshal .Schemes }},
"swagger": "2.0",
"info": {
"description": "{{escape .Description}}",
"title": "{{.Title}}",
"contact": {},
"version": "{{.Version}}"
},
"host": "{{.Host}}",
"basePath": "{{.BasePath}}",
"paths": {
"/families": {
"post": {
"description": "Создает новую семью",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"Families"
],
"summary": "Создать семью",
"parameters": [
{
"description": "Family info",
"name": "family",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/domain.CreateFamilyRequest"
}
}
],
"responses": {
"201": {
"description": "Created",
"schema": {
"$ref": "#/definitions/domain.FamilyResponse"
}
},
"400": {
"description": "invalid body",
"schema": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
},
"500": {
"description": "internal server error",
"schema": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
}
}
}
},
"/families/{id}": {
"get": {
"description": "Возвращает семью по ее внутреннему ID",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"Families"
],
"summary": "Получить семью по ID",
"parameters": [
{
"type": "integer",
"description": "Family ID",
"name": "id",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/domain.FamilyResponse"
}
},
"400": {
"description": "invalid id",
"schema": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
},
"404": {
"description": "family not found",
"schema": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
},
"500": {
"description": "internal server error",
"schema": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
}
}
},
"delete": {
"description": "Удаляет семью по ее ID",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"Families"
],
"summary": "Удалить семью",
"parameters": [
{
"type": "integer",
"description": "Family ID",
"name": "id",
"in": "path",
"required": true
}
],
"responses": {
"204": {
"description": "no content",
"schema": {
"type": "string"
}
},
"400": {
"description": "invalid id",
"schema": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
},
"404": {
"description": "family not found",
"schema": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
},
"500": {
"description": "internal server error",
"schema": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
}
}
},
"patch": {
"description": "Частично обновляет данные семьи по ID",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"Families"
],
"summary": "Обновить семью",
"parameters": [
{
"type": "integer",
"description": "Family ID",
"name": "id",
"in": "path",
"required": true
},
{
"description": "Данные для обновления",
"name": "family",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/domain.UpdateFamilyRequest"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/domain.FamilyResponse"
}
},
"400": {
"description": "name is required",
"schema": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
},
"404": {
"description": "family not found",
"schema": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
},
"500": {
"description": "internal server error",
"schema": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
}
}
}
},
"/users": {
"post": {
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"Users"
],
"summary": "Создать пользователя",
"parameters": [
{
"description": "User info",
"name": "user",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/domain.CreateUserRequest"
}
}
],
"responses": {
"201": {
"description": "Created",
"schema": {
"$ref": "#/definitions/domain.UserResponse"
}
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/domain.UserErrorResponse"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/domain.UserErrorResponse"
}
}
}
}
},
"/users/by-telegram/{telegramId}": {
"get": {
"description": "Возвращает пользователя по его Telegram ID",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"Users"
],
"summary": "Получить пользователя по Telegram ID",
"parameters": [
{
"type": "integer",
"description": "Telegram ID",
"name": "telegramId",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/domain.UserResponse"
}
},
"400": {
"description": "invalid telegram id",
"schema": {
"$ref": "#/definitions/domain.UserErrorResponse"
}
},
"404": {
"description": "user not found",
"schema": {
"$ref": "#/definitions/domain.UserErrorResponse"
}
},
"500": {
"description": "internal server error",
"schema": {
"$ref": "#/definitions/domain.UserErrorResponse"
}
}
}
}
},
"/users/{id}": {
"get": {
"description": "Возвращает пользователя по его внутреннему ID",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"Users"
],
"summary": "Получить пользователя по ID",
"parameters": [
{
"type": "integer",
"description": "User ID",
"name": "id",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/domain.UserResponse"
}
},
"400": {
"description": "invalid id",
"schema": {
"$ref": "#/definitions/domain.UserErrorResponse"
}
},
"404": {
"description": "user not found",
"schema": {
"$ref": "#/definitions/domain.UserErrorResponse"
}
},
"500": {
"description": "internal server error",
"schema": {
"$ref": "#/definitions/domain.UserErrorResponse"
}
}
}
},
"delete": {
"description": "Удаляет пользователя по его ID",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"Users"
],
"summary": "Удалить пользователя",
"parameters": [
{
"type": "integer",
"description": "User ID",
"name": "id",
"in": "path",
"required": true
}
],
"responses": {
"204": {
"description": "no content",
"schema": {
"type": "string"
}
},
"400": {
"description": "invalid id",
"schema": {
"$ref": "#/definitions/domain.UserErrorResponse"
}
},
"404": {
"description": "user not found",
"schema": {
"$ref": "#/definitions/domain.UserErrorResponse"
}
},
"500": {
"description": "internal server error",
"schema": {
"$ref": "#/definitions/domain.UserErrorResponse"
}
}
}
},
"patch": {
"description": "Частично обновляет данные пользователя по ID",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"Users"
],
"summary": "Обновить пользователя",
"parameters": [
{
"type": "integer",
"description": "User ID",
"name": "id",
"in": "path",
"required": true
},
{
"description": "Данные для обновления",
"name": "user",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/domain.UpdateUserRequest"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/domain.UserResponse"
}
},
"400": {
"description": "invalid id or invalid body",
"schema": {
"$ref": "#/definitions/domain.UserErrorResponse"
}
},
"404": {
"description": "user not found",
"schema": {
"$ref": "#/definitions/domain.UserErrorResponse"
}
},
"500": {
"description": "internal server error",
"schema": {
"$ref": "#/definitions/domain.UserErrorResponse"
}
}
}
}
}
},
"definitions": {
"domain.CreateFamilyRequest": {
"type": "object",
"properties": {
"name": {
"type": "string"
},
"owner_id": {
"type": "integer"
},
"telegram_chat_id": {
"type": "integer"
},
"telegram_chat_name": {
"type": "string"
}
}
},
"domain.CreateUserRequest": {
"type": "object",
"required": [
"first_name",
"telegram_id"
],
"properties": {
"first_name": {
"type": "string"
},
"language_code": {
"type": "string"
},
"last_name": {
"type": "string"
},
"telegram_id": {
"type": "integer"
},
"username": {
"type": "string"
}
}
},
"domain.FamilyResponse": {
"type": "object",
"properties": {
"created_at": {
"type": "string"
},
"id": {
"type": "integer"
},
"name": {
"type": "string"
},
"owner_id": {
"type": "integer"
},
"telegram_chat_id": {
"type": "integer"
},
"telegram_chat_name": {
"type": "string"
},
"updated_at": {
"type": "string"
}
}
},
"domain.UpdateFamilyRequest": {
"type": "object",
"properties": {
"name": {
"type": "string"
},
"telegram_chat_name": {
"type": "string"
}
}
},
"domain.UpdateUserRequest": {
"type": "object",
"properties": {
"first_name": {
"type": "string"
},
"language_code": {
"type": "string"
},
"last_name": {
"type": "string"
},
"username": {
"type": "string"
}
}
},
"domain.UserErrorResponse": {
"type": "object",
"properties": {
"error": {
"type": "string"
}
}
},
"domain.UserResponse": {
"type": "object",
"properties": {
"created_at": {
"type": "string"
},
"first_name": {
"type": "string"
},
"id": {
"type": "integer"
},
"language_code": {
"type": "string"
},
"last_name": {
"type": "string"
},
"telegram_id": {
"type": "integer"
},
"updated_at": {
"type": "string"
},
"username": {
"type": "string"
}
}
}
}
}`
// SwaggerInfo holds exported Swagger Info so clients can modify it
var SwaggerInfo = &swag.Spec{
Version: "",
Host: "",
BasePath: "",
Schemes: []string{},
Title: "",
Description: "",
InfoInstanceName: "swagger",
SwaggerTemplate: docTemplate,
LeftDelim: "{{",
RightDelim: "}}",
}
func init() {
swag.Register(SwaggerInfo.InstanceName(), SwaggerInfo)
}
+626
View File
@@ -0,0 +1,626 @@
{
"swagger": "2.0",
"info": {
"contact": {}
},
"paths": {
"/families": {
"post": {
"description": "Создает новую семью",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"Families"
],
"summary": "Создать семью",
"parameters": [
{
"description": "Family info",
"name": "family",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/domain.CreateFamilyRequest"
}
}
],
"responses": {
"201": {
"description": "Created",
"schema": {
"$ref": "#/definitions/domain.FamilyResponse"
}
},
"400": {
"description": "invalid body",
"schema": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
},
"500": {
"description": "internal server error",
"schema": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
}
}
}
},
"/families/{id}": {
"get": {
"description": "Возвращает семью по ее внутреннему ID",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"Families"
],
"summary": "Получить семью по ID",
"parameters": [
{
"type": "integer",
"description": "Family ID",
"name": "id",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/domain.FamilyResponse"
}
},
"400": {
"description": "invalid id",
"schema": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
},
"404": {
"description": "family not found",
"schema": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
},
"500": {
"description": "internal server error",
"schema": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
}
}
},
"delete": {
"description": "Удаляет семью по ее ID",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"Families"
],
"summary": "Удалить семью",
"parameters": [
{
"type": "integer",
"description": "Family ID",
"name": "id",
"in": "path",
"required": true
}
],
"responses": {
"204": {
"description": "no content",
"schema": {
"type": "string"
}
},
"400": {
"description": "invalid id",
"schema": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
},
"404": {
"description": "family not found",
"schema": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
},
"500": {
"description": "internal server error",
"schema": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
}
}
},
"patch": {
"description": "Частично обновляет данные семьи по ID",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"Families"
],
"summary": "Обновить семью",
"parameters": [
{
"type": "integer",
"description": "Family ID",
"name": "id",
"in": "path",
"required": true
},
{
"description": "Данные для обновления",
"name": "family",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/domain.UpdateFamilyRequest"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/domain.FamilyResponse"
}
},
"400": {
"description": "name is required",
"schema": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
},
"404": {
"description": "family not found",
"schema": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
},
"500": {
"description": "internal server error",
"schema": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
}
}
}
},
"/users": {
"post": {
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"Users"
],
"summary": "Создать пользователя",
"parameters": [
{
"description": "User info",
"name": "user",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/domain.CreateUserRequest"
}
}
],
"responses": {
"201": {
"description": "Created",
"schema": {
"$ref": "#/definitions/domain.UserResponse"
}
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/domain.UserErrorResponse"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/domain.UserErrorResponse"
}
}
}
}
},
"/users/by-telegram/{telegramId}": {
"get": {
"description": "Возвращает пользователя по его Telegram ID",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"Users"
],
"summary": "Получить пользователя по Telegram ID",
"parameters": [
{
"type": "integer",
"description": "Telegram ID",
"name": "telegramId",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/domain.UserResponse"
}
},
"400": {
"description": "invalid telegram id",
"schema": {
"$ref": "#/definitions/domain.UserErrorResponse"
}
},
"404": {
"description": "user not found",
"schema": {
"$ref": "#/definitions/domain.UserErrorResponse"
}
},
"500": {
"description": "internal server error",
"schema": {
"$ref": "#/definitions/domain.UserErrorResponse"
}
}
}
}
},
"/users/{id}": {
"get": {
"description": "Возвращает пользователя по его внутреннему ID",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"Users"
],
"summary": "Получить пользователя по ID",
"parameters": [
{
"type": "integer",
"description": "User ID",
"name": "id",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/domain.UserResponse"
}
},
"400": {
"description": "invalid id",
"schema": {
"$ref": "#/definitions/domain.UserErrorResponse"
}
},
"404": {
"description": "user not found",
"schema": {
"$ref": "#/definitions/domain.UserErrorResponse"
}
},
"500": {
"description": "internal server error",
"schema": {
"$ref": "#/definitions/domain.UserErrorResponse"
}
}
}
},
"delete": {
"description": "Удаляет пользователя по его ID",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"Users"
],
"summary": "Удалить пользователя",
"parameters": [
{
"type": "integer",
"description": "User ID",
"name": "id",
"in": "path",
"required": true
}
],
"responses": {
"204": {
"description": "no content",
"schema": {
"type": "string"
}
},
"400": {
"description": "invalid id",
"schema": {
"$ref": "#/definitions/domain.UserErrorResponse"
}
},
"404": {
"description": "user not found",
"schema": {
"$ref": "#/definitions/domain.UserErrorResponse"
}
},
"500": {
"description": "internal server error",
"schema": {
"$ref": "#/definitions/domain.UserErrorResponse"
}
}
}
},
"patch": {
"description": "Частично обновляет данные пользователя по ID",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"Users"
],
"summary": "Обновить пользователя",
"parameters": [
{
"type": "integer",
"description": "User ID",
"name": "id",
"in": "path",
"required": true
},
{
"description": "Данные для обновления",
"name": "user",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/domain.UpdateUserRequest"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/domain.UserResponse"
}
},
"400": {
"description": "invalid id or invalid body",
"schema": {
"$ref": "#/definitions/domain.UserErrorResponse"
}
},
"404": {
"description": "user not found",
"schema": {
"$ref": "#/definitions/domain.UserErrorResponse"
}
},
"500": {
"description": "internal server error",
"schema": {
"$ref": "#/definitions/domain.UserErrorResponse"
}
}
}
}
}
},
"definitions": {
"domain.CreateFamilyRequest": {
"type": "object",
"properties": {
"name": {
"type": "string"
},
"owner_id": {
"type": "integer"
},
"telegram_chat_id": {
"type": "integer"
},
"telegram_chat_name": {
"type": "string"
}
}
},
"domain.CreateUserRequest": {
"type": "object",
"required": [
"first_name",
"telegram_id"
],
"properties": {
"first_name": {
"type": "string"
},
"language_code": {
"type": "string"
},
"last_name": {
"type": "string"
},
"telegram_id": {
"type": "integer"
},
"username": {
"type": "string"
}
}
},
"domain.FamilyResponse": {
"type": "object",
"properties": {
"created_at": {
"type": "string"
},
"id": {
"type": "integer"
},
"name": {
"type": "string"
},
"owner_id": {
"type": "integer"
},
"telegram_chat_id": {
"type": "integer"
},
"telegram_chat_name": {
"type": "string"
},
"updated_at": {
"type": "string"
}
}
},
"domain.UpdateFamilyRequest": {
"type": "object",
"properties": {
"name": {
"type": "string"
},
"telegram_chat_name": {
"type": "string"
}
}
},
"domain.UpdateUserRequest": {
"type": "object",
"properties": {
"first_name": {
"type": "string"
},
"language_code": {
"type": "string"
},
"last_name": {
"type": "string"
},
"username": {
"type": "string"
}
}
},
"domain.UserErrorResponse": {
"type": "object",
"properties": {
"error": {
"type": "string"
}
}
},
"domain.UserResponse": {
"type": "object",
"properties": {
"created_at": {
"type": "string"
},
"first_name": {
"type": "string"
},
"id": {
"type": "integer"
},
"language_code": {
"type": "string"
},
"last_name": {
"type": "string"
},
"telegram_id": {
"type": "integer"
},
"updated_at": {
"type": "string"
},
"username": {
"type": "string"
}
}
}
}
}
+411
View File
@@ -0,0 +1,411 @@
definitions:
domain.CreateFamilyRequest:
properties:
name:
type: string
owner_id:
type: integer
telegram_chat_id:
type: integer
telegram_chat_name:
type: string
type: object
domain.CreateUserRequest:
properties:
first_name:
type: string
language_code:
type: string
last_name:
type: string
telegram_id:
type: integer
username:
type: string
required:
- first_name
- telegram_id
type: object
domain.FamilyResponse:
properties:
created_at:
type: string
id:
type: integer
name:
type: string
owner_id:
type: integer
telegram_chat_id:
type: integer
telegram_chat_name:
type: string
updated_at:
type: string
type: object
domain.UpdateFamilyRequest:
properties:
name:
type: string
telegram_chat_name:
type: string
type: object
domain.UpdateUserRequest:
properties:
first_name:
type: string
language_code:
type: string
last_name:
type: string
username:
type: string
type: object
domain.UserErrorResponse:
properties:
error:
type: string
type: object
domain.UserResponse:
properties:
created_at:
type: string
first_name:
type: string
id:
type: integer
language_code:
type: string
last_name:
type: string
telegram_id:
type: integer
updated_at:
type: string
username:
type: string
type: object
info:
contact: {}
paths:
/families:
post:
consumes:
- application/json
description: Создает новую семью
parameters:
- description: Family info
in: body
name: family
required: true
schema:
$ref: '#/definitions/domain.CreateFamilyRequest'
produces:
- application/json
responses:
"201":
description: Created
schema:
$ref: '#/definitions/domain.FamilyResponse'
"400":
description: invalid body
schema:
additionalProperties:
type: string
type: object
"500":
description: internal server error
schema:
additionalProperties:
type: string
type: object
summary: Создать семью
tags:
- Families
/families/{id}:
delete:
consumes:
- application/json
description: Удаляет семью по ее ID
parameters:
- description: Family ID
in: path
name: id
required: true
type: integer
produces:
- application/json
responses:
"204":
description: no content
schema:
type: string
"400":
description: invalid id
schema:
additionalProperties:
type: string
type: object
"404":
description: family not found
schema:
additionalProperties:
type: string
type: object
"500":
description: internal server error
schema:
additionalProperties:
type: string
type: object
summary: Удалить семью
tags:
- Families
get:
consumes:
- application/json
description: Возвращает семью по ее внутреннему ID
parameters:
- description: Family ID
in: path
name: id
required: true
type: integer
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/domain.FamilyResponse'
"400":
description: invalid id
schema:
additionalProperties:
type: string
type: object
"404":
description: family not found
schema:
additionalProperties:
type: string
type: object
"500":
description: internal server error
schema:
additionalProperties:
type: string
type: object
summary: Получить семью по ID
tags:
- Families
patch:
consumes:
- application/json
description: Частично обновляет данные семьи по ID
parameters:
- description: Family ID
in: path
name: id
required: true
type: integer
- description: Данные для обновления
in: body
name: family
required: true
schema:
$ref: '#/definitions/domain.UpdateFamilyRequest'
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/domain.FamilyResponse'
"400":
description: name is required
schema:
additionalProperties:
type: string
type: object
"404":
description: family not found
schema:
additionalProperties:
type: string
type: object
"500":
description: internal server error
schema:
additionalProperties:
type: string
type: object
summary: Обновить семью
tags:
- Families
/users:
post:
consumes:
- application/json
parameters:
- description: User info
in: body
name: user
required: true
schema:
$ref: '#/definitions/domain.CreateUserRequest'
produces:
- application/json
responses:
"201":
description: Created
schema:
$ref: '#/definitions/domain.UserResponse'
"400":
description: Bad Request
schema:
$ref: '#/definitions/domain.UserErrorResponse'
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/domain.UserErrorResponse'
summary: Создать пользователя
tags:
- Users
/users/{id}:
delete:
consumes:
- application/json
description: Удаляет пользователя по его ID
parameters:
- description: User ID
in: path
name: id
required: true
type: integer
produces:
- application/json
responses:
"204":
description: no content
schema:
type: string
"400":
description: invalid id
schema:
$ref: '#/definitions/domain.UserErrorResponse'
"404":
description: user not found
schema:
$ref: '#/definitions/domain.UserErrorResponse'
"500":
description: internal server error
schema:
$ref: '#/definitions/domain.UserErrorResponse'
summary: Удалить пользователя
tags:
- Users
get:
consumes:
- application/json
description: Возвращает пользователя по его внутреннему ID
parameters:
- description: User ID
in: path
name: id
required: true
type: integer
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/domain.UserResponse'
"400":
description: invalid id
schema:
$ref: '#/definitions/domain.UserErrorResponse'
"404":
description: user not found
schema:
$ref: '#/definitions/domain.UserErrorResponse'
"500":
description: internal server error
schema:
$ref: '#/definitions/domain.UserErrorResponse'
summary: Получить пользователя по ID
tags:
- Users
patch:
consumes:
- application/json
description: Частично обновляет данные пользователя по ID
parameters:
- description: User ID
in: path
name: id
required: true
type: integer
- description: Данные для обновления
in: body
name: user
required: true
schema:
$ref: '#/definitions/domain.UpdateUserRequest'
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/domain.UserResponse'
"400":
description: invalid id or invalid body
schema:
$ref: '#/definitions/domain.UserErrorResponse'
"404":
description: user not found
schema:
$ref: '#/definitions/domain.UserErrorResponse'
"500":
description: internal server error
schema:
$ref: '#/definitions/domain.UserErrorResponse'
summary: Обновить пользователя
tags:
- Users
/users/by-telegram/{telegramId}:
get:
consumes:
- application/json
description: Возвращает пользователя по его Telegram ID
parameters:
- description: Telegram ID
in: path
name: telegramId
required: true
type: integer
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/domain.UserResponse'
"400":
description: invalid telegram id
schema:
$ref: '#/definitions/domain.UserErrorResponse'
"404":
description: user not found
schema:
$ref: '#/definitions/domain.UserErrorResponse'
"500":
description: internal server error
schema:
$ref: '#/definitions/domain.UserErrorResponse'
summary: Получить пользователя по Telegram ID
tags:
- Users
swagger: "2.0"
+5
View File
@@ -0,0 +1,5 @@
package dto
type ErrorResponse struct {
Message string `json:"message"`
}
+26
View File
@@ -0,0 +1,26 @@
package routers
import (
"FamilyHub/src/api/services"
"github.com/gin-gonic/gin"
)
type AuthRouter struct {
service services.AuthService
}
func NewAuthRouter(s services.AuthService) *AuthRouter {
return &AuthRouter{service: s}
}
func (router *AuthRouter) RegisterRouter(r *gin.RouterGroup) {
auth := r.Group("/auth")
{
auth.POST("", router.Auth)
}
}
func (router *AuthRouter) Auth(c *gin.Context) {
}
+169
View File
@@ -0,0 +1,169 @@
package routers
import (
"FamilyHub/src/api/services"
"FamilyHub/src/domain"
"database/sql"
"errors"
"net/http"
"strconv"
"github.com/gin-gonic/gin"
)
type FamiliesRouter struct {
service services.FamilyService
}
func NewFamiliesRouter(s services.FamilyService) *FamiliesRouter {
return &FamiliesRouter{service: s}
}
func (router *FamiliesRouter) RegisterRoutes(r *gin.RouterGroup) {
families := r.Group("/families")
{
families.POST("", router.Create)
families.GET("/:id", router.GetByID)
families.PATCH("/:id", router.Update)
families.DELETE("/:id", router.Delete)
}
}
// Create GoDoc
// @Summary Создать семью
// @Description Создает новую семью
// @Tags Families
// @Accept json
// @Produce json
// @Param family body domain.CreateFamilyRequest true "Family info"
// @Success 201 {object} domain.FamilyResponse
// @Failure 400 {object} map[string]string "invalid body"
// @Failure 500 {object} map[string]string "internal server error"
// @Router /families [post]
func (router *FamiliesRouter) Create(c *gin.Context) {
var req domain.CreateFamilyRequest
var resp domain.FamilyResponse
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
family, err := router.service.Create(c.Request.Context(), req)
if err != nil {
handleFamilyError(c, err)
return
}
c.JSON(http.StatusCreated, resp.ModelToResponse(family))
}
// GetByID GoDoc
// @Summary Получить семью по ID
// @Description Возвращает семью по ее внутреннему ID
// @Tags Families
// @Accept json
// @Produce json
// @Param id path int true "Family ID"
// @Success 200 {object} domain.FamilyResponse
// @Failure 400 {object} map[string]string "invalid id"
// @Failure 404 {object} map[string]string "family not found"
// @Failure 500 {object} map[string]string "internal server error"
// @Router /families/{id} [get]
func (router *FamiliesRouter) GetByID(c *gin.Context) {
var resp domain.FamilyResponse
id, err := strconv.ParseInt(c.Param("id"), 10, 64)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid id"})
return
}
family, err := router.service.GetByID(c.Request.Context(), id)
if err != nil {
handleFamilyError(c, err)
return
}
c.JSON(http.StatusOK, resp.ModelToResponse(family))
}
// Update GoDoc
// @Summary Обновить семью
// @Description Частично обновляет данные семьи по ID
// @Tags Families
// @Accept json
// @Produce json
// @Param id path int true "Family ID"
// @Param family body domain.UpdateFamilyRequest true "Данные для обновления"
// @Success 200 {object} domain.FamilyResponse
// @Failure 400 {object} map[string]string "invalid id or invalid body"
// @Failure 400 {object} map[string]string "name is required"
// @Failure 404 {object} map[string]string "family not found"
// @Failure 500 {object} map[string]string "internal server error"
// @Router /families/{id} [patch]
func (router *FamiliesRouter) Update(c *gin.Context) {
var resp domain.FamilyResponse
id, err := strconv.ParseInt(c.Param("id"), 10, 64)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid id"})
return
}
var req domain.UpdateFamilyRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
if req.Name == nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "name is required"})
return
}
family, err := router.service.Update(c.Request.Context(), id, req)
if err != nil {
handleFamilyError(c, err)
return
}
c.JSON(http.StatusOK, resp.ModelToResponse(family))
}
// Delete GoDoc
// @Summary Удалить семью
// @Description Удаляет семью по ее ID
// @Tags Families
// @Accept json
// @Produce json
// @Param id path int true "Family ID"
// @Success 204 {string} string "no content"
// @Failure 400 {object} map[string]string "invalid id"
// @Failure 404 {object} map[string]string "family not found"
// @Failure 500 {object} map[string]string "internal server error"
// @Router /families/{id} [delete]
func (router *FamiliesRouter) Delete(c *gin.Context) {
id, err := strconv.ParseInt(c.Param("id"), 10, 64)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid id"})
return
}
if err := router.service.Delete(c.Request.Context(), id); err != nil {
handleFamilyError(c, err)
return
}
c.Status(http.StatusNoContent)
}
func handleFamilyError(c *gin.Context, err error) {
switch {
case errors.Is(err, services.ErrFamilyNotFound):
c.JSON(http.StatusNotFound, gin.H{"error": err.Error()})
case errors.Is(err, sql.ErrNoRows):
c.JSON(http.StatusNotFound, gin.H{"error": "family not found"})
default:
c.JSON(http.StatusInternalServerError, gin.H{"error": "internal server error"})
}
}
+315
View File
@@ -0,0 +1,315 @@
package routers
import (
"FamilyHub/src/api/services"
"FamilyHub/src/domain"
"bytes"
"context"
"database/sql"
"encoding/json"
"errors"
"net/http"
"net/http/httptest"
"strings"
"testing"
"time"
"github.com/gin-gonic/gin"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
type familyServiceMock struct {
createFn func(ctx context.Context, req domain.CreateFamilyRequest) (*domain.Family, error)
getByIDFn func(ctx context.Context, id int64) (*domain.Family, error)
updateFn func(ctx context.Context, id int64, req domain.UpdateFamilyRequest) (*domain.Family, error)
deleteFn func(ctx context.Context, id int64) error
}
func (m *familyServiceMock) Create(ctx context.Context, req domain.CreateFamilyRequest) (*domain.Family, error) {
if m.createFn != nil {
return m.createFn(ctx, req)
}
return nil, errors.New("mock create is not configured")
}
func (m *familyServiceMock) GetByID(ctx context.Context, id int64) (*domain.Family, error) {
if m.getByIDFn != nil {
return m.getByIDFn(ctx, id)
}
return nil, errors.New("mock getByID is not configured")
}
func (m *familyServiceMock) Update(ctx context.Context, id int64, req domain.UpdateFamilyRequest) (*domain.Family, error) {
if m.updateFn != nil {
return m.updateFn(ctx, id, req)
}
return nil, errors.New("mock update is not configured")
}
func (m *familyServiceMock) Delete(ctx context.Context, id int64) error {
if m.deleteFn != nil {
return m.deleteFn(ctx, id)
}
return errors.New("mock delete is not configured")
}
func setupFamiliesRouter(mock services.FamilyService) *gin.Engine {
gin.SetMode(gin.TestMode)
r := gin.New()
apiV1 := r.Group("/api/v1")
router := NewFamiliesRouter(mock)
router.RegisterRoutes(apiV1)
return r
}
func sampleFamily() *domain.Family {
return &domain.Family{
ID: 7,
Name: "Belan",
OwnerID: 10,
TelegramChatID: 12345,
TelegramChatName: "Family Chat",
CreatedAt: time.Date(2026, time.January, 21, 10, 0, 0, 0, time.UTC),
UpdatedAt: time.Date(2026, time.January, 21, 11, 0, 0, 0, time.UTC),
}
}
func TestFamiliesRouter_Create(t *testing.T) {
t.Run("bad request on malformed body", func(t *testing.T) {
r := setupFamiliesRouter(&familyServiceMock{})
req := httptest.NewRequest(http.MethodPost, "/api/v1/families", bytes.NewBufferString(`{"name":`))
req.Header.Set("Content-Type", "application/json")
w := httptest.NewRecorder()
r.ServeHTTP(w, req)
require.Equal(t, http.StatusBadRequest, w.Code)
assert.Contains(t, w.Body.String(), "error")
})
t.Run("internal error", func(t *testing.T) {
r := setupFamiliesRouter(&familyServiceMock{createFn: func(ctx context.Context, req domain.CreateFamilyRequest) (*domain.Family, error) {
return nil, errors.New("db unavailable")
}})
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.Header.Set("Content-Type", "application/json")
w := httptest.NewRecorder()
r.ServeHTTP(w, req)
require.Equal(t, http.StatusInternalServerError, w.Code)
assert.Contains(t, w.Body.String(), "internal server error")
})
t.Run("created", func(t *testing.T) {
expected := sampleFamily()
r := setupFamiliesRouter(&familyServiceMock{createFn: func(ctx context.Context, req domain.CreateFamilyRequest) (*domain.Family, error) {
assert.Equal(t, "Belan", req.Name)
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.Header.Set("Content-Type", "application/json")
w := httptest.NewRecorder()
r.ServeHTTP(w, req)
require.Equal(t, http.StatusCreated, w.Code)
assert.Contains(t, w.Body.String(), "Belan")
})
}
func TestFamiliesRouter_GetByID(t *testing.T) {
t.Run("bad request on invalid id", func(t *testing.T) {
r := setupFamiliesRouter(&familyServiceMock{})
req := httptest.NewRequest(http.MethodGet, "/api/v1/families/abc", nil)
w := httptest.NewRecorder()
r.ServeHTTP(w, req)
require.Equal(t, http.StatusBadRequest, w.Code)
assert.Contains(t, w.Body.String(), "invalid id")
})
t.Run("not found on service error", func(t *testing.T) {
r := setupFamiliesRouter(&familyServiceMock{getByIDFn: func(ctx context.Context, id int64) (*domain.Family, error) {
return nil, services.ErrFamilyNotFound
}})
req := httptest.NewRequest(http.MethodGet, "/api/v1/families/7", nil)
w := httptest.NewRecorder()
r.ServeHTTP(w, req)
require.Equal(t, http.StatusNotFound, w.Code)
assert.Contains(t, w.Body.String(), services.ErrFamilyNotFound.Error())
})
t.Run("not found on sql no rows", func(t *testing.T) {
r := setupFamiliesRouter(&familyServiceMock{getByIDFn: func(ctx context.Context, id int64) (*domain.Family, error) {
return nil, sql.ErrNoRows
}})
req := httptest.NewRequest(http.MethodGet, "/api/v1/families/7", nil)
w := httptest.NewRecorder()
r.ServeHTTP(w, req)
require.Equal(t, http.StatusNotFound, w.Code)
assert.Contains(t, w.Body.String(), "family not found")
})
t.Run("ok", func(t *testing.T) {
expected := sampleFamily()
r := setupFamiliesRouter(&familyServiceMock{getByIDFn: func(ctx context.Context, id int64) (*domain.Family, error) {
assert.Equal(t, int64(7), id)
return expected, nil
}})
req := httptest.NewRequest(http.MethodGet, "/api/v1/families/7", nil)
w := httptest.NewRecorder()
r.ServeHTTP(w, req)
require.Equal(t, http.StatusOK, w.Code)
assert.Contains(t, w.Body.String(), "Belan")
})
}
func TestFamiliesRouter_Update(t *testing.T) {
t.Run("bad request on invalid id", func(t *testing.T) {
r := setupFamiliesRouter(&familyServiceMock{})
req := httptest.NewRequest(http.MethodPatch, "/api/v1/families/abc", bytes.NewBufferString(`{"name":"Belan"}`))
req.Header.Set("Content-Type", "application/json")
w := httptest.NewRecorder()
r.ServeHTTP(w, req)
require.Equal(t, http.StatusBadRequest, w.Code)
assert.Contains(t, w.Body.String(), "invalid id")
})
t.Run("bad request on malformed body", func(t *testing.T) {
r := setupFamiliesRouter(&familyServiceMock{})
req := httptest.NewRequest(http.MethodPatch, "/api/v1/families/7", bytes.NewBufferString(`{"name":`))
req.Header.Set("Content-Type", "application/json")
w := httptest.NewRecorder()
r.ServeHTTP(w, req)
require.Equal(t, http.StatusBadRequest, w.Code)
assert.Contains(t, w.Body.String(), "error")
})
t.Run("bad request on missing name", func(t *testing.T) {
r := setupFamiliesRouter(&familyServiceMock{})
req := httptest.NewRequest(http.MethodPatch, "/api/v1/families/7", bytes.NewBufferString(`{"telegram_chat_name":"Updated"}`))
req.Header.Set("Content-Type", "application/json")
w := httptest.NewRecorder()
r.ServeHTTP(w, req)
require.Equal(t, http.StatusBadRequest, w.Code)
assert.Contains(t, w.Body.String(), "name is required")
})
t.Run("not found", func(t *testing.T) {
r := setupFamiliesRouter(&familyServiceMock{updateFn: func(ctx context.Context, id int64, req domain.UpdateFamilyRequest) (*domain.Family, error) {
return nil, services.ErrFamilyNotFound
}})
name := "Belan Updated"
req := httptest.NewRequest(http.MethodPatch, "/api/v1/families/7", bytes.NewBufferString(`{"name":"`+name+`","telegram_chat_name":"Updated"}`))
req.Header.Set("Content-Type", "application/json")
w := httptest.NewRecorder()
r.ServeHTTP(w, req)
require.Equal(t, http.StatusNotFound, w.Code)
assert.Contains(t, w.Body.String(), services.ErrFamilyNotFound.Error())
})
t.Run("ok", func(t *testing.T) {
expected := sampleFamily()
updatedName := "Belan Updated"
expected.Name = updatedName
r := setupFamiliesRouter(&familyServiceMock{updateFn: func(ctx context.Context, id int64, req domain.UpdateFamilyRequest) (*domain.Family, error) {
assert.Equal(t, int64(7), id)
require.NotNil(t, req.Name)
assert.Equal(t, updatedName, *req.Name)
assert.Equal(t, "Updated", req.TelegramChatName)
return expected, nil
}})
req := httptest.NewRequest(http.MethodPatch, "/api/v1/families/7", bytes.NewBufferString(`{"name":"`+updatedName+`","telegram_chat_name":"Updated"}`))
req.Header.Set("Content-Type", "application/json")
w := httptest.NewRecorder()
r.ServeHTTP(w, req)
require.Equal(t, http.StatusOK, w.Code)
assert.Contains(t, w.Body.String(), updatedName)
})
}
func TestFamiliesRouter_Delete(t *testing.T) {
t.Run("bad request on invalid id", func(t *testing.T) {
r := setupFamiliesRouter(&familyServiceMock{})
req := httptest.NewRequest(http.MethodDelete, "/api/v1/families/abc", nil)
w := httptest.NewRecorder()
r.ServeHTTP(w, req)
require.Equal(t, http.StatusBadRequest, w.Code)
assert.Contains(t, w.Body.String(), "invalid id")
})
t.Run("not found", func(t *testing.T) {
r := setupFamiliesRouter(&familyServiceMock{deleteFn: func(ctx context.Context, id int64) error {
return sql.ErrNoRows
}})
req := httptest.NewRequest(http.MethodDelete, "/api/v1/families/7", nil)
w := httptest.NewRecorder()
r.ServeHTTP(w, req)
require.Equal(t, http.StatusNotFound, w.Code)
assert.Contains(t, w.Body.String(), "family not found")
})
t.Run("no content", func(t *testing.T) {
called := false
r := setupFamiliesRouter(&familyServiceMock{deleteFn: func(ctx context.Context, id int64) error {
called = true
assert.Equal(t, int64(7), id)
return nil
}})
req := httptest.NewRequest(http.MethodDelete, "/api/v1/families/7", nil)
w := httptest.NewRecorder()
r.ServeHTTP(w, req)
require.Equal(t, http.StatusNoContent, w.Code)
assert.True(t, called)
assert.Empty(t, strings.TrimSpace(w.Body.String()))
})
}
func TestFamiliesRouter_Create_ResponseShape(t *testing.T) {
expected := sampleFamily()
r := setupFamiliesRouter(&familyServiceMock{createFn: func(ctx context.Context, req domain.CreateFamilyRequest) (*domain.Family, error) {
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.Header.Set("Content-Type", "application/json")
w := httptest.NewRecorder()
r.ServeHTTP(w, req)
require.Equal(t, http.StatusCreated, w.Code)
var resp domain.FamilyResponse
err := json.Unmarshal(w.Body.Bytes(), &resp)
require.NoError(t, err)
assert.Equal(t, expected.ID, resp.ID)
assert.Equal(t, expected.Name, resp.Name)
assert.Equal(t, expected.OwnerID, resp.OwnerID)
assert.Equal(t, expected.TelegramChatID, resp.TelegramChatID)
assert.Equal(t, expected.TelegramChatName, resp.TelegramChatName)
assert.Equal(t, expected.CreatedAt.Format(time.RFC3339), resp.CreatedAt)
assert.Equal(t, expected.UpdatedAt.Format(time.RFC3339), resp.UpdatedAt)
}
+62
View File
@@ -0,0 +1,62 @@
package routers
import (
"FamilyHub/src/api/dto"
"FamilyHub/src/domain"
"FamilyHub/src/utils"
"context"
"log"
"net/http"
"time"
"github.com/gin-gonic/gin"
)
type receiptService interface {
GetReceipt(ctx context.Context, date string, number string) (*domain.Receipt, error)
}
type ReceiptRouter struct {
service receiptService
}
func NewReceiptRouter(s receiptService) *ReceiptRouter {
return &ReceiptRouter{service: s}
}
func (router *ReceiptRouter) RegisterRoutes(r *gin.RouterGroup) {
receipts := r.Group("/receipts")
{
receipts.POST("", router.AddReceipt)
}
}
func (router *ReceiptRouter) AddReceipt(context_ *gin.Context) {
var req domain.AddReceiptRequest
if err := context_.ShouldBindJSON(&req); err != nil {
log.Println("bind error:", err)
context_.JSON(http.StatusBadRequest, dto.ErrorResponse{Message: err.Error()})
return
}
isoDate, err := utils.NormalizeDateToISO(req.Date)
if err != nil {
context_.JSON(400, gin.H{"error": "invalid date format"})
return
}
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
defer cancel()
receipt, err := router.service.GetReceipt(ctx, isoDate, req.Number)
if err != nil {
context_.JSON(400, gin.H{"error": err.Error()})
log.Printf("API error, %s", err.Error())
return
}
resp := domain.AddReceiptResponse{
ID: 1,
Number: receipt.ReceiptNumber,
Date: receipt.IssuedAt,
}
context_.JSON(http.StatusOK, resp)
}
+117
View File
@@ -0,0 +1,117 @@
package routers
import (
"FamilyHub/src/domain"
"bytes"
"context"
"encoding/json"
"errors"
"net/http"
"net/http/httptest"
"strings"
"testing"
"time"
"github.com/gin-gonic/gin"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
type receiptServiceMock struct {
getReceiptFn func(ctx context.Context, date string, number string) (*domain.Receipt, error)
}
func (m *receiptServiceMock) GetReceipt(ctx context.Context, date string, number string) (*domain.Receipt, error) {
if m.getReceiptFn != nil {
return m.getReceiptFn(ctx, date, number)
}
return nil, errors.New("mock is not configured")
}
func TestReceiptRouter_AddReceipt(t *testing.T) {
gin.SetMode(gin.TestMode)
validNumber := strings.Repeat("1", 24)
validDate := "21.01.2026"
expectedDate := "2026-01-21"
now := time.Date(2026, time.January, 21, 10, 11, 12, 0, time.UTC)
tests := []struct {
name string
body string
mock *receiptServiceMock
expectedStatus int
expectedContains string
}{
{
name: "bad request on invalid body",
body: `{"date":"21.01.2026"}`,
mock: &receiptServiceMock{},
expectedStatus: http.StatusBadRequest,
expectedContains: "Number",
},
{
name: "bad request on invalid date format",
body: `{"number":"` + validNumber + `","date":"2026-01-21"}`,
mock: &receiptServiceMock{},
expectedStatus: http.StatusBadRequest,
expectedContains: "invalid date format",
},
{
name: "bad request on service error",
body: `{"number":"` + validNumber + `","date":"` + validDate + `"}`,
mock: &receiptServiceMock{getReceiptFn: func(ctx context.Context, date string, number string) (*domain.Receipt, error) {
assert.Equal(t, expectedDate, date)
assert.Equal(t, validNumber, number)
return nil, errors.New("receipt not found")
}},
expectedStatus: http.StatusBadRequest,
expectedContains: "receipt not found",
},
{
name: "ok",
body: `{"number":"` + validNumber + `","date":"` + validDate + `"}`,
mock: &receiptServiceMock{getReceiptFn: func(ctx context.Context, date string, number string) (*domain.Receipt, error) {
assert.Equal(t, expectedDate, date)
assert.Equal(t, validNumber, number)
return &domain.Receipt{ReceiptNumber: validNumber, IssuedAt: now}, nil
}},
expectedStatus: http.StatusOK,
expectedContains: validNumber,
},
}
for _, tc := range tests {
tc := tc
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
r := gin.New()
apiV1 := r.Group("/api/v1")
router := NewReceiptRouter(tc.mock)
router.RegisterRoutes(apiV1)
req := httptest.NewRequest(http.MethodPost, "/api/v1/receipts", bytes.NewBufferString(tc.body))
req.Header.Set("Content-Type", "application/json")
w := httptest.NewRecorder()
r.ServeHTTP(w, req)
require.Equal(t, tc.expectedStatus, w.Code)
assert.Contains(t, w.Body.String(), tc.expectedContains)
if tc.expectedStatus == http.StatusOK {
var resp struct {
ID int32 `json:"id"`
Number string `json:"number"`
Date time.Time `json:"date"`
}
err := json.Unmarshal(w.Body.Bytes(), &resp)
require.NoError(t, err)
assert.Equal(t, int32(1), resp.ID)
assert.Equal(t, validNumber, resp.Number)
assert.Equal(t, now, resp.Date)
}
})
}
}
+192
View File
@@ -0,0 +1,192 @@
package routers
import (
"FamilyHub/src/api/services"
"FamilyHub/src/domain"
"errors"
"net/http"
"strconv"
"github.com/gin-gonic/gin"
)
type UsersRouter struct {
service services.UserService
}
func NewUsersRouter(s services.UserService) *UsersRouter {
return &UsersRouter{service: s}
}
func (router *UsersRouter) RegisterRoutes(r *gin.RouterGroup) {
users := r.Group("/users")
{
users.POST("", router.CreateUser)
users.GET("/:id", router.GetByID)
users.GET("/by-telegram/:telegramId", router.GetByTelegramID)
users.PATCH("/:id", router.Update)
users.DELETE("/:id", router.Delete)
}
}
// CreateUser GoDoc
// @Summary Создать пользователя
// @Tags Users
// @Accept json
// @Produce json
// @Param user body domain.CreateUserRequest true "User info"
// @Success 201 {object} domain.UserResponse
// @Failure 400 {object} domain.UserErrorResponse
// @Failure 500 {object} domain.UserErrorResponse
// @Router /users [post]
func (router *UsersRouter) CreateUser(c *gin.Context) {
var req domain.CreateUserRequest
var resp domain.UserResponse
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, domain.UserErrorResponse{Error: err.Error()})
return
}
user, err := router.service.Create(c.Request.Context(), req)
if err != nil {
handleError(c, err)
return
}
c.JSON(http.StatusCreated, resp.ModelToResponse(user))
}
// GetByID GoDoc
// @Summary Получить пользователя по ID
// @Description Возвращает пользователя по его внутреннему ID
// @Tags Users
// @Accept json
// @Produce json
// @Param id path int true "User ID"
// @Success 200 {object} domain.UserResponse
// @Failure 400 {object} domain.UserErrorResponse "invalid id"
// @Failure 404 {object} domain.UserErrorResponse "user not found"
// @Failure 500 {object} domain.UserErrorResponse "internal server error"
// @Router /users/{id} [get]
func (router *UsersRouter) GetByID(c *gin.Context) {
var resp domain.UserResponse
id, err := strconv.ParseInt(c.Param("id"), 10, 64)
if err != nil {
c.JSON(http.StatusBadRequest, domain.UserErrorResponse{Error: "invalid id"})
return
}
user, err := router.service.GetByID(c.Request.Context(), id)
if err != nil {
handleError(c, err)
return
}
c.JSON(http.StatusOK, resp.ModelToResponse(user))
}
// GetByTelegramID GoDoc
// @Summary Получить пользователя по Telegram ID
// @Description Возвращает пользователя по его Telegram ID
// @Tags Users
// @Accept json
// @Produce json
// @Param telegramId path int true "Telegram ID"
// @Success 200 {object} domain.UserResponse
// @Failure 400 {object} domain.UserErrorResponse "invalid telegram id"
// @Failure 404 {object} domain.UserErrorResponse "user not found"
// @Failure 500 {object} domain.UserErrorResponse "internal server error"
// @Router /users/by-telegram/{telegramId} [get]
func (router *UsersRouter) GetByTelegramID(c *gin.Context) {
var resp domain.UserResponse
telegramID, err := strconv.ParseInt(c.Param("telegramId"), 10, 64)
if err != nil {
c.JSON(http.StatusBadRequest, domain.UserErrorResponse{Error: "invalid telegram id"})
return
}
user, err := router.service.GetByTelegramID(c.Request.Context(), telegramID)
if err != nil {
handleError(c, err)
return
}
c.JSON(http.StatusOK, resp.ModelToResponse(user))
}
// Update GoDoc
// @Summary Обновить пользователя
// @Description Частично обновляет данные пользователя по ID
// @Tags Users
// @Accept json
// @Produce json
// @Param id path int true "User ID"
// @Param user body domain.UpdateUserRequest true "Данные для обновления"
// @Success 200 {object} domain.UserResponse
// @Failure 400 {object} domain.UserErrorResponse "invalid id or invalid body"
// @Failure 404 {object} domain.UserErrorResponse "user not found"
// @Failure 500 {object} domain.UserErrorResponse "internal server error"
// @Router /users/{id} [patch]
func (router *UsersRouter) Update(c *gin.Context) {
var resp domain.UserResponse
id, err := strconv.ParseInt(c.Param("id"), 10, 64)
if err != nil {
c.JSON(http.StatusBadRequest, domain.UserErrorResponse{Error: "invalid id"})
return
}
var req domain.UpdateUserRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, domain.UserErrorResponse{Error: err.Error()})
return
}
user, err := router.service.Update(c.Request.Context(), id, req)
if err != nil {
handleError(c, err)
return
}
c.JSON(http.StatusOK, resp.ModelToResponse(user))
}
// Delete GoDoc
// @Summary Удалить пользователя
// @Description Удаляет пользователя по его ID
// @Tags Users
// @Accept json
// @Produce json
// @Param id path int true "User ID"
// @Success 204 {string} string "no content"
// @Failure 400 {object} domain.UserErrorResponse "invalid id"
// @Failure 404 {object} domain.UserErrorResponse "user not found"
// @Failure 500 {object} domain.UserErrorResponse "internal server error"
// @Router /users/{id} [delete]
func (router *UsersRouter) Delete(c *gin.Context) {
id, err := strconv.ParseInt(c.Param("id"), 10, 64)
if err != nil {
c.JSON(http.StatusBadRequest, domain.UserErrorResponse{Error: "invalid id"})
return
}
if err := router.service.Delete(c.Request.Context(), id); err != nil {
handleError(c, err)
return
}
c.Status(http.StatusNoContent)
}
func handleError(c *gin.Context, err error) {
switch {
case errors.Is(err, services.ErrUserNotFound):
c.JSON(http.StatusNotFound, domain.UserErrorResponse{Error: err.Error()})
case errors.Is(err, services.ErrInvalidPatch):
c.JSON(http.StatusBadRequest, domain.UserErrorResponse{Error: err.Error()})
case errors.Is(err, services.ErrTelegramIDMissing):
c.JSON(http.StatusBadRequest, domain.UserErrorResponse{Error: err.Error()})
default:
c.JSON(http.StatusInternalServerError, domain.UserErrorResponse{Error: "internal server error"})
}
}
+345
View File
@@ -0,0 +1,345 @@
package routers
import (
"FamilyHub/src/api/services"
"FamilyHub/src/domain"
"bytes"
"context"
"encoding/json"
"errors"
"net/http"
"net/http/httptest"
"strconv"
"strings"
"testing"
"time"
"github.com/gin-gonic/gin"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
type userServiceMock struct {
createFn func(ctx context.Context, req domain.CreateUserRequest) (*domain.UserModel, error)
getByIDFn func(ctx context.Context, id int64) (*domain.UserModel, error)
getByTelegramIDFn func(ctx context.Context, telegramID int64) (*domain.UserModel, error)
updateFn func(ctx context.Context, id int64, req domain.UpdateUserRequest) (*domain.UserModel, error)
deleteFn func(ctx context.Context, id int64) error
}
func (m *userServiceMock) Create(ctx context.Context, req domain.CreateUserRequest) (*domain.UserModel, error) {
if m.createFn != nil {
return m.createFn(ctx, req)
}
return nil, errors.New("mock create is not configured")
}
func (m *userServiceMock) GetByID(ctx context.Context, id int64) (*domain.UserModel, error) {
if m.getByIDFn != nil {
return m.getByIDFn(ctx, id)
}
return nil, errors.New("mock getByID is not configured")
}
func (m *userServiceMock) GetByTelegramID(ctx context.Context, telegramID int64) (*domain.UserModel, error) {
if m.getByTelegramIDFn != nil {
return m.getByTelegramIDFn(ctx, telegramID)
}
return nil, errors.New("mock getByTelegramID is not configured")
}
func (m *userServiceMock) Update(ctx context.Context, id int64, req domain.UpdateUserRequest) (*domain.UserModel, error) {
if m.updateFn != nil {
return m.updateFn(ctx, id, req)
}
return nil, errors.New("mock update is not configured")
}
func (m *userServiceMock) Delete(ctx context.Context, id int64) error {
if m.deleteFn != nil {
return m.deleteFn(ctx, id)
}
return errors.New("mock delete is not configured")
}
func setupUsersRouter(mock services.UserService) *gin.Engine {
gin.SetMode(gin.TestMode)
r := gin.New()
apiV1 := r.Group("/api/v1")
router := NewUsersRouter(mock)
router.RegisterRoutes(apiV1)
return r
}
func sampleUser() *domain.UserModel {
username := "john"
firstName := "John"
lastName := "Doe"
languageCode := "en"
return &domain.UserModel{
ID: 10,
TelegramID: 100500,
Username: &username,
FirstName: &firstName,
LastName: &lastName,
LanguageCode: &languageCode,
CreatedAt: time.Date(2026, time.January, 21, 10, 11, 12, 0, time.UTC),
UpdatedAt: time.Date(2026, time.January, 21, 12, 11, 12, 0, time.UTC),
}
}
func TestUsersRouter_CreateUser(t *testing.T) {
t.Run("bad request on malformed body", func(t *testing.T) {
r := setupUsersRouter(&userServiceMock{})
req := httptest.NewRequest(http.MethodPost, "/api/v1/users", bytes.NewBufferString(`{"telegram_id":`))
req.Header.Set("Content-Type", "application/json")
w := httptest.NewRecorder()
r.ServeHTTP(w, req)
require.Equal(t, http.StatusBadRequest, w.Code)
assert.Contains(t, w.Body.String(), "error")
})
t.Run("bad request on domain validation error", func(t *testing.T) {
r := setupUsersRouter(&userServiceMock{createFn: func(ctx context.Context, req domain.CreateUserRequest) (*domain.UserModel, error) {
return nil, services.ErrTelegramIDMissing
}})
req := httptest.NewRequest(http.MethodPost, "/api/v1/users", bytes.NewBufferString(`{"telegram_id":1,"first_name":"A"}`))
req.Header.Set("Content-Type", "application/json")
w := httptest.NewRecorder()
r.ServeHTTP(w, req)
require.Equal(t, http.StatusBadRequest, w.Code)
assert.Contains(t, w.Body.String(), services.ErrTelegramIDMissing.Error())
})
t.Run("created", func(t *testing.T) {
expected := sampleUser()
r := setupUsersRouter(&userServiceMock{createFn: func(ctx context.Context, req domain.CreateUserRequest) (*domain.UserModel, error) {
assert.Equal(t, int64(100500), req.TelegramID)
require.NotNil(t, req.FirstName)
assert.Equal(t, "John", *req.FirstName)
return expected, nil
}})
req := httptest.NewRequest(http.MethodPost, "/api/v1/users", bytes.NewBufferString(`{"telegram_id":100500,"first_name":"John"}`))
req.Header.Set("Content-Type", "application/json")
w := httptest.NewRecorder()
r.ServeHTTP(w, req)
require.Equal(t, http.StatusCreated, w.Code)
assert.Contains(t, w.Body.String(), "100500")
assert.Contains(t, w.Body.String(), "John")
})
}
func TestUsersRouter_GetByID(t *testing.T) {
t.Run("bad request on invalid id", func(t *testing.T) {
r := setupUsersRouter(&userServiceMock{})
req := httptest.NewRequest(http.MethodGet, "/api/v1/users/abc", nil)
w := httptest.NewRecorder()
r.ServeHTTP(w, req)
require.Equal(t, http.StatusBadRequest, w.Code)
assert.Contains(t, w.Body.String(), "invalid id")
})
t.Run("not found", func(t *testing.T) {
r := setupUsersRouter(&userServiceMock{getByIDFn: func(ctx context.Context, id int64) (*domain.UserModel, error) {
return nil, services.ErrUserNotFound
}})
req := httptest.NewRequest(http.MethodGet, "/api/v1/users/1", nil)
w := httptest.NewRecorder()
r.ServeHTTP(w, req)
require.Equal(t, http.StatusNotFound, w.Code)
assert.Contains(t, w.Body.String(), services.ErrUserNotFound.Error())
})
t.Run("ok", func(t *testing.T) {
expected := sampleUser()
r := setupUsersRouter(&userServiceMock{getByIDFn: func(ctx context.Context, id int64) (*domain.UserModel, error) {
assert.Equal(t, int64(10), id)
return expected, nil
}})
req := httptest.NewRequest(http.MethodGet, "/api/v1/users/10", nil)
w := httptest.NewRecorder()
r.ServeHTTP(w, req)
require.Equal(t, http.StatusOK, w.Code)
assert.Contains(t, w.Body.String(), "100500")
})
}
func TestUsersRouter_GetByTelegramID(t *testing.T) {
t.Run("bad request on invalid telegram id", func(t *testing.T) {
r := setupUsersRouter(&userServiceMock{})
req := httptest.NewRequest(http.MethodGet, "/api/v1/users/by-telegram/abc", nil)
w := httptest.NewRecorder()
r.ServeHTTP(w, req)
require.Equal(t, http.StatusBadRequest, w.Code)
assert.Contains(t, w.Body.String(), "invalid telegram id")
})
t.Run("ok", func(t *testing.T) {
expected := sampleUser()
r := setupUsersRouter(&userServiceMock{getByTelegramIDFn: func(ctx context.Context, telegramID int64) (*domain.UserModel, error) {
assert.Equal(t, int64(100500), telegramID)
return expected, nil
}})
req := httptest.NewRequest(http.MethodGet, "/api/v1/users/by-telegram/100500", nil)
w := httptest.NewRecorder()
r.ServeHTTP(w, req)
require.Equal(t, http.StatusOK, w.Code)
assert.Contains(t, w.Body.String(), "John")
})
}
func TestUsersRouter_Update(t *testing.T) {
t.Run("bad request on invalid id", func(t *testing.T) {
r := setupUsersRouter(&userServiceMock{})
req := httptest.NewRequest(http.MethodPatch, "/api/v1/users/abc", bytes.NewBufferString(`{"first_name":"John"}`))
req.Header.Set("Content-Type", "application/json")
w := httptest.NewRecorder()
r.ServeHTTP(w, req)
require.Equal(t, http.StatusBadRequest, w.Code)
assert.Contains(t, w.Body.String(), "invalid id")
})
t.Run("bad request on malformed body", func(t *testing.T) {
r := setupUsersRouter(&userServiceMock{})
req := httptest.NewRequest(http.MethodPatch, "/api/v1/users/10", bytes.NewBufferString(`{"first_name":`))
req.Header.Set("Content-Type", "application/json")
w := httptest.NewRecorder()
r.ServeHTTP(w, req)
require.Equal(t, http.StatusBadRequest, w.Code)
assert.Contains(t, w.Body.String(), "error")
})
t.Run("bad request on invalid patch", func(t *testing.T) {
r := setupUsersRouter(&userServiceMock{updateFn: func(ctx context.Context, id int64, req domain.UpdateUserRequest) (*domain.UserModel, error) {
return nil, services.ErrInvalidPatch
}})
req := httptest.NewRequest(http.MethodPatch, "/api/v1/users/10", bytes.NewBufferString(`{"first_name":"John"}`))
req.Header.Set("Content-Type", "application/json")
w := httptest.NewRecorder()
r.ServeHTTP(w, req)
require.Equal(t, http.StatusBadRequest, w.Code)
assert.Contains(t, w.Body.String(), services.ErrInvalidPatch.Error())
})
t.Run("ok", func(t *testing.T) {
expected := sampleUser()
r := setupUsersRouter(&userServiceMock{updateFn: func(ctx context.Context, id int64, req domain.UpdateUserRequest) (*domain.UserModel, error) {
assert.Equal(t, int64(10), id)
require.NotNil(t, req.FirstName)
assert.Equal(t, "John", *req.FirstName)
return expected, nil
}})
req := httptest.NewRequest(http.MethodPatch, "/api/v1/users/10", bytes.NewBufferString(`{"first_name":"John"}`))
req.Header.Set("Content-Type", "application/json")
w := httptest.NewRecorder()
r.ServeHTTP(w, req)
require.Equal(t, http.StatusOK, w.Code)
assert.Contains(t, w.Body.String(), "100500")
})
}
func TestUsersRouter_Delete(t *testing.T) {
t.Run("bad request on invalid id", func(t *testing.T) {
r := setupUsersRouter(&userServiceMock{})
req := httptest.NewRequest(http.MethodDelete, "/api/v1/users/abc", nil)
w := httptest.NewRecorder()
r.ServeHTTP(w, req)
require.Equal(t, http.StatusBadRequest, w.Code)
assert.Contains(t, w.Body.String(), "invalid id")
})
t.Run("not found", func(t *testing.T) {
r := setupUsersRouter(&userServiceMock{deleteFn: func(ctx context.Context, id int64) error {
return services.ErrUserNotFound
}})
req := httptest.NewRequest(http.MethodDelete, "/api/v1/users/1", nil)
w := httptest.NewRecorder()
r.ServeHTTP(w, req)
require.Equal(t, http.StatusNotFound, w.Code)
assert.Contains(t, w.Body.String(), services.ErrUserNotFound.Error())
})
t.Run("no content", func(t *testing.T) {
called := false
r := setupUsersRouter(&userServiceMock{deleteFn: func(ctx context.Context, id int64) error {
called = true
assert.Equal(t, int64(10), id)
return nil
}})
req := httptest.NewRequest(http.MethodDelete, "/api/v1/users/10", nil)
w := httptest.NewRecorder()
r.ServeHTTP(w, req)
require.Equal(t, http.StatusNoContent, w.Code)
assert.True(t, called)
assert.Empty(t, strings.TrimSpace(w.Body.String()))
})
}
func TestUsersRouter_CreateUser_ResponseShape(t *testing.T) {
expected := sampleUser()
r := setupUsersRouter(&userServiceMock{createFn: func(ctx context.Context, req domain.CreateUserRequest) (*domain.UserModel, error) {
return expected, nil
}})
req := httptest.NewRequest(http.MethodPost, "/api/v1/users", bytes.NewBufferString(`{"telegram_id":100500,"first_name":"John"}`))
req.Header.Set("Content-Type", "application/json")
w := httptest.NewRecorder()
r.ServeHTTP(w, req)
require.Equal(t, http.StatusCreated, w.Code)
var resp domain.UserResponse
err := json.Unmarshal(w.Body.Bytes(), &resp)
require.NoError(t, err)
assert.Equal(t, expected.ID, resp.ID)
assert.Equal(t, expected.TelegramID, resp.TelegramID)
assert.Equal(t, expected.FirstName, resp.FirstName)
assert.Equal(t, expected.CreatedAt.Format(time.RFC3339), resp.CreatedAt)
assert.Equal(t, expected.UpdatedAt.Format(time.RFC3339), resp.UpdatedAt)
}
func TestUsersRouter_GetByID_UsesPathID(t *testing.T) {
r := setupUsersRouter(&userServiceMock{getByIDFn: func(ctx context.Context, id int64) (*domain.UserModel, error) {
assert.Equal(t, int64(42), id)
u := sampleUser()
u.ID = id
return u, nil
}})
req := httptest.NewRequest(http.MethodGet, "/api/v1/users/42", nil)
w := httptest.NewRecorder()
r.ServeHTTP(w, req)
require.Equal(t, http.StatusOK, w.Code)
assert.Contains(t, w.Body.String(), strconv.FormatInt(42, 10))
}
+78
View File
@@ -0,0 +1,78 @@
package api
import (
_ "FamilyHub/src/api/docs"
"FamilyHub/src/api/routers"
"FamilyHub/src/api/services"
"FamilyHub/src/config"
"FamilyHub/src/database"
"FamilyHub/src/integrations/receiptService"
"FamilyHub/src/repositories"
"context"
"log"
"net/http"
"github.com/gin-gonic/gin"
swaggerFiles "github.com/swaggo/files"
ginSwagger "github.com/swaggo/gin-swagger"
)
type Server struct {
httpServer *http.Server
}
func NewServer(cfg config.Config) *Server {
dbManager := &database.Database{
ConnectionString: cfg.DBConnectionString,
MigrationsPath: "file://migrations",
}
dbConn, err := dbManager.Connect()
if err != nil {
log.Fatal(err)
}
if err := dbManager.RunMigrations(dbConn); err != nil {
log.Fatal(err)
}
router := gin.Default()
if cfg.OpenAPIEnabled {
router.GET("/openapi/*any", ginSwagger.WrapHandler(swaggerFiles.Handler))
}
apiV1 := router.Group("/api/v1")
receiptRepo := repositories.NewReceiptsSQLRepository(dbConn)
receiptService_ := receiptService.NewReceiptService(receiptRepo)
receiptRouter := routers.NewReceiptRouter(receiptService_)
receiptRouter.RegisterRoutes(apiV1)
usersRepo := repositories.NewUsersSQLRepository(dbConn)
usersService := services.NewUserService(usersRepo)
usersRouter := routers.NewUsersRouter(usersService)
usersRouter.RegisterRoutes(apiV1)
familyRepo := repositories.NewFamilySQLRepository(dbConn)
familyService := services.NewFamilyService(familyRepo)
familyRouter := routers.NewFamiliesRouter(familyService)
familyRouter.RegisterRoutes(apiV1)
otpRepo := repositories.NewOTPSQLRepository(dbConn)
authService := services.NewAuthService(usersRepo, otpRepo)
authRouter := routers.NewAuthRouter(authService)
authRouter.RegisterRouter(apiV1)
return &Server{
httpServer: &http.Server{
Addr: cfg.APIHost + ":" + cfg.APIPort,
Handler: router,
},
}
}
func (s *Server) Start() error {
return s.httpServer.ListenAndServe()
}
func (s *Server) Shutdown(ctx context.Context) error {
return s.httpServer.Shutdown(ctx)
}
+131
View File
@@ -0,0 +1,131 @@
package services
import (
"FamilyHub/src/config"
"FamilyHub/src/domain"
"FamilyHub/src/repositories"
"FamilyHub/src/utils"
"context"
"crypto/hmac"
"crypto/rand"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"net/url"
"sort"
"strings"
"time"
)
type AuthService interface {
AuthByTelegram(ctx context.Context, initData string) (string, error)
CreateOTP(ctx context.Context, telegramId int64) error
}
type authService struct {
usersRepo repositories.UsersRepository
otpRepo repositories.OTPRepository
config config.Config
jwt *utils.JWTManager
}
func NewAuthService(usersRepo repositories.UsersRepository, otpRepo repositories.OTPRepository) AuthService {
return &authService{
usersRepo: usersRepo,
otpRepo: otpRepo,
}
}
var (
ErrWrongOtp = errors.New("wrong otp")
ErrForbidden = errors.New("forbidden")
ErrUnauthorized = errors.New("unauthorized")
)
func (s *authService) AuthByTelegram(ctx context.Context, initData string) (string, error) {
data, ok := ValidateTelegramInitData(initData, s.config.BotToken)
if !ok {
return "", ErrUnauthorized
}
var user struct {
ID int64 `json:"id"`
}
err := json.Unmarshal([]byte(data["user"]), &user)
if err != nil {
return "", err
}
userModel, err := s.usersRepo.GetByTelegramID(ctx, user.ID)
if err != nil {
return "", err
}
return s.jwt.Generate(userModel.ID)
}
func (s *authService) CreateOTP(ctx context.Context, telegramId int64) error {
user, err := s.usersRepo.GetByTelegramID(ctx, telegramId)
if err != nil {
return err
}
if user == nil {
return ErrForbidden
}
b := make([]byte, 3)
if _, err = rand.Read(b); err != nil {
return err
}
code := fmt.Sprintf("%06d", (int(b[0])<<16|int(b[1])<<8|int(b[2]))%1000000)
otp := &domain.OTP{
UserID: user.ID,
Code: code,
ExpiredAt: time.Now().Add(10 * time.Minute),
}
err = s.otpRepo.Create(ctx, otp)
if err != nil {
return err
}
return nil
}
func ValidateTelegramInitData(initData string, botToken string) (map[string]string, bool) {
values, err := url.ParseQuery(initData)
if err != nil {
return nil, false
}
hash := values.Get("hash")
values.Del("hash")
var dataCheck []string
for k, v := range values {
dataCheck = append(dataCheck, k+"="+v[0])
}
sort.Strings(dataCheck)
dataCheckString := strings.Join(dataCheck, "\n")
secret := sha256.Sum256([]byte(botToken))
h := hmac.New(sha256.New, secret[:])
h.Write([]byte(dataCheckString))
expectedHash := hex.EncodeToString(h.Sum(nil))
return mapFromValues(values), expectedHash == hash
}
func mapFromValues(v url.Values) map[string]string {
m := make(map[string]string)
for k, val := range v {
m[k] = val[0]
}
return m
}
+77
View File
@@ -0,0 +1,77 @@
package services
import (
"FamilyHub/src/domain"
"FamilyHub/src/repositories"
"context"
"errors"
)
type FamilyService interface {
Create(ctx context.Context, req domain.CreateFamilyRequest) (*domain.Family, error)
GetByID(ctx context.Context, id int64) (*domain.Family, error)
Update(ctx context.Context, id int64, req domain.UpdateFamilyRequest) (*domain.Family, error)
Delete(ctx context.Context, id int64) error
}
type familyService struct {
repo repositories.FamilyRepository
}
func NewFamilyService(repo repositories.FamilyRepository) FamilyService {
return &familyService{repo: repo}
}
var (
ErrFamilyNotFound = errors.New("family not found")
)
func (s *familyService) Create(ctx context.Context, req domain.CreateFamilyRequest) (*domain.Family, error) {
family_ := &domain.Family{
Name: req.Name,
OwnerID: req.OwnerID,
TelegramChatID: req.TelegramChatID,
TelegramChatName: req.TelegramChatName,
}
if err := s.repo.Create(ctx, family_); err != nil {
return nil, err
}
return family_, nil
}
func (s *familyService) GetByID(ctx context.Context, id int64) (*domain.Family, error) {
family_, err := s.repo.GetByID(ctx, id)
if err != nil {
return nil, err
}
if family_ == nil {
return nil, ErrFamilyNotFound
}
return family_, nil
}
func (s *familyService) Update(ctx context.Context, id int64, req domain.UpdateFamilyRequest) (*domain.Family, error) {
existing, err := s.repo.GetByID(ctx, id)
if err != nil {
return nil, err
}
if existing == nil {
return nil, ErrFamilyNotFound
}
if err := s.repo.Update(ctx, &domain.Family{
ID: id,
Name: *req.Name,
OwnerID: existing.OwnerID,
TelegramChatID: existing.TelegramChatID,
TelegramChatName: req.TelegramChatName,
}); err != nil {
return nil, err
}
return s.repo.GetByID(ctx, id)
}
func (s *familyService) Delete(ctx context.Context, id int64) error {
if err := s.repo.Delete(ctx, id); err != nil {
return err
}
return nil
}
+106
View File
@@ -0,0 +1,106 @@
package services
import (
"FamilyHub/src/domain"
"FamilyHub/src/repositories"
"context"
"errors"
)
type UserService interface {
Create(ctx context.Context, req domain.CreateUserRequest) (*domain.UserModel, error)
GetByID(ctx context.Context, id int64) (*domain.UserModel, error)
GetByTelegramID(ctx context.Context, telegramID int64) (*domain.UserModel, error)
Update(ctx context.Context, id int64, req domain.UpdateUserRequest) (*domain.UserModel, error)
Delete(ctx context.Context, id int64) error
}
type userService struct {
repo repositories.UsersRepository
}
func NewUserService(repo repositories.UsersRepository) UserService {
return &userService{repo: repo}
}
var (
ErrUserNotFound = errors.New("user not found")
ErrInvalidPatch = errors.New("empty update payload")
ErrTelegramIDMissing = errors.New("telegram_id is required")
)
func (s *userService) Create(ctx context.Context, req domain.CreateUserRequest) (*domain.UserModel, error) {
user_ := &domain.UserModel{
TelegramID: req.TelegramID,
Username: req.Username,
FirstName: req.FirstName,
LastName: req.LastName,
LanguageCode: req.LanguageCode,
}
if err := s.repo.Create(ctx, user_); err != nil {
return nil, err
}
return user_, nil
}
func (s *userService) GetByID(ctx context.Context, id int64) (*domain.UserModel, error) {
user, err := s.repo.GetByID(ctx, id)
if err != nil {
return nil, err
}
if user == nil {
return nil, ErrUserNotFound
}
return user, nil
}
func (s *userService) GetByTelegramID(ctx context.Context, telegramID int64) (*domain.UserModel, error) {
user, err := s.repo.GetByTelegramID(ctx, telegramID)
if err != nil {
return nil, err
}
if user == nil {
return nil, ErrUserNotFound
}
return user, nil
}
func (s *userService) Update(ctx context.Context, id int64, req domain.UpdateUserRequest) (*domain.UserModel, error) {
existing, err := s.repo.GetByID(ctx, id)
if err != nil {
return nil, err
}
if existing == nil {
return nil, ErrUserNotFound
}
if req.Username == nil &&
req.FirstName == nil &&
req.LastName == nil &&
req.LanguageCode == nil {
return nil, ErrInvalidPatch
}
if err := s.repo.Update(ctx, &domain.UserModel{
ID: id,
Username: req.Username,
FirstName: req.FirstName,
LastName: req.LastName,
LanguageCode: req.LanguageCode,
}); err != nil {
return nil, err
}
return s.repo.GetByID(ctx, id)
}
func (s *userService) Delete(ctx context.Context, id int64) error {
user, err := s.repo.GetByID(ctx, id)
if err != nil {
return err
}
if user == nil {
return ErrUserNotFound
}
return s.repo.Delete(ctx, id)
}