Renamed and updated project.
This commit is contained in:
+3
-1
@@ -2,4 +2,6 @@
|
|||||||
.idea
|
.idea
|
||||||
.env
|
.env
|
||||||
secret_key.json
|
secret_key.json
|
||||||
data
|
data
|
||||||
|
archive
|
||||||
|
volumes
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
version: '3.9'
|
||||||
|
|
||||||
|
services:
|
||||||
|
db:
|
||||||
|
image: postgres:16
|
||||||
|
container_name: postgres
|
||||||
|
restart: always
|
||||||
|
environment:
|
||||||
|
POSTGRES_USER: familyUser
|
||||||
|
POSTGRES_PASSWORD: familyPass
|
||||||
|
POSTGRES_DB: familyHubDB
|
||||||
|
ports:
|
||||||
|
- "5432:5432"
|
||||||
|
volumes:
|
||||||
|
- ./volumes/postgres:/var/lib/postgresql/data
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
module GoFinanceManager
|
module FamilyHub
|
||||||
|
|
||||||
go 1.25
|
go 1.25
|
||||||
|
|
||||||
@@ -8,7 +8,9 @@ require (
|
|||||||
github.com/go-telegram-bot-api/telegram-bot-api/v5 v5.5.1
|
github.com/go-telegram-bot-api/telegram-bot-api/v5 v5.5.1
|
||||||
github.com/golang-migrate/migrate/v4 v4.19.1
|
github.com/golang-migrate/migrate/v4 v4.19.1
|
||||||
github.com/joho/godotenv v1.5.1
|
github.com/joho/godotenv v1.5.1
|
||||||
github.com/mattn/go-sqlite3 v1.14.33
|
github.com/lib/pq v1.10.9
|
||||||
|
github.com/liyue201/goqr v0.0.0-20200803022322-df443203d4ea
|
||||||
|
github.com/mattn/go-sqlite3 v1.14.34
|
||||||
github.com/stretchr/testify v1.11.1
|
github.com/stretchr/testify v1.11.1
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -19,19 +21,20 @@ require (
|
|||||||
cloud.google.com/go/compute/metadata v0.8.0 // indirect
|
cloud.google.com/go/compute/metadata v0.8.0 // indirect
|
||||||
cloud.google.com/go/longrunning v0.6.7 // indirect
|
cloud.google.com/go/longrunning v0.6.7 // indirect
|
||||||
cloud.google.com/go/vision/v2 v2.9.5 // indirect
|
cloud.google.com/go/vision/v2 v2.9.5 // indirect
|
||||||
github.com/bytedance/sonic v1.14.0 // indirect
|
github.com/bytedance/gopkg v0.1.3 // indirect
|
||||||
github.com/bytedance/sonic/loader v0.3.0 // indirect
|
github.com/bytedance/sonic v1.14.2 // indirect
|
||||||
|
github.com/bytedance/sonic/loader v0.4.0 // indirect
|
||||||
github.com/cloudwego/base64x v0.1.6 // indirect
|
github.com/cloudwego/base64x v0.1.6 // indirect
|
||||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
|
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
|
||||||
github.com/felixge/httpsnoop v1.0.4 // indirect
|
github.com/felixge/httpsnoop v1.0.4 // indirect
|
||||||
github.com/gabriel-vasile/mimetype v1.4.8 // indirect
|
github.com/gabriel-vasile/mimetype v1.4.13 // indirect
|
||||||
github.com/gin-contrib/sse v1.1.0 // indirect
|
github.com/gin-contrib/sse v1.1.0 // indirect
|
||||||
github.com/go-logr/logr v1.4.3 // indirect
|
github.com/go-logr/logr v1.4.3 // indirect
|
||||||
github.com/go-logr/stdr v1.2.2 // indirect
|
github.com/go-logr/stdr v1.2.2 // indirect
|
||||||
github.com/go-playground/locales v0.14.1 // indirect
|
github.com/go-playground/locales v0.14.1 // indirect
|
||||||
github.com/go-playground/universal-translator v0.18.1 // indirect
|
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||||
github.com/go-playground/validator/v10 v10.27.0 // indirect
|
github.com/go-playground/validator/v10 v10.27.0 // indirect
|
||||||
github.com/goccy/go-json v0.10.2 // indirect
|
github.com/goccy/go-json v0.10.5 // indirect
|
||||||
github.com/goccy/go-yaml v1.18.0 // indirect
|
github.com/goccy/go-yaml v1.18.0 // indirect
|
||||||
github.com/google/s2a-go v0.1.9 // indirect
|
github.com/google/s2a-go v0.1.9 // indirect
|
||||||
github.com/googleapis/enterprise-certificate-proxy v0.3.6 // indirect
|
github.com/googleapis/enterprise-certificate-proxy v0.3.6 // indirect
|
||||||
@@ -40,36 +43,33 @@ require (
|
|||||||
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
|
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
|
||||||
github.com/leodido/go-urn v1.4.0 // indirect
|
github.com/leodido/go-urn v1.4.0 // indirect
|
||||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 // indirect
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||||
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
|
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
|
||||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
|
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
|
||||||
github.com/quic-go/qpack v0.5.1 // indirect
|
github.com/quic-go/qpack v0.6.0 // indirect
|
||||||
github.com/quic-go/quic-go v0.54.0 // indirect
|
github.com/quic-go/quic-go v0.57.1 // indirect
|
||||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||||
github.com/ugorji/go/codec v1.3.0 // indirect
|
github.com/ugorji/go/codec v1.3.1 // indirect
|
||||||
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
|
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
|
||||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0 // indirect
|
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0 // indirect
|
||||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 // indirect
|
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 // indirect
|
||||||
go.opentelemetry.io/otel v1.37.0 // indirect
|
go.opentelemetry.io/otel v1.37.0 // indirect
|
||||||
go.opentelemetry.io/otel/metric v1.37.0 // indirect
|
go.opentelemetry.io/otel/metric v1.37.0 // indirect
|
||||||
go.opentelemetry.io/otel/trace v1.37.0 // indirect
|
go.opentelemetry.io/otel/trace v1.37.0 // indirect
|
||||||
go.uber.org/mock v0.5.0 // indirect
|
|
||||||
golang.org/x/arch v0.20.0 // indirect
|
golang.org/x/arch v0.20.0 // indirect
|
||||||
golang.org/x/crypto v0.45.0 // indirect
|
golang.org/x/crypto v0.45.0 // indirect
|
||||||
golang.org/x/mod v0.29.0 // indirect
|
|
||||||
golang.org/x/net v0.47.0 // indirect
|
golang.org/x/net v0.47.0 // indirect
|
||||||
golang.org/x/oauth2 v0.30.0 // indirect
|
golang.org/x/oauth2 v0.30.0 // indirect
|
||||||
golang.org/x/sync v0.18.0 // indirect
|
golang.org/x/sync v0.18.0 // indirect
|
||||||
golang.org/x/sys v0.38.0 // indirect
|
golang.org/x/sys v0.38.0 // indirect
|
||||||
golang.org/x/text v0.31.0 // indirect
|
golang.org/x/text v0.31.0 // indirect
|
||||||
golang.org/x/time v0.12.0 // indirect
|
golang.org/x/time v0.12.0 // indirect
|
||||||
golang.org/x/tools v0.38.0 // indirect
|
|
||||||
google.golang.org/api v0.247.0 // indirect
|
google.golang.org/api v0.247.0 // indirect
|
||||||
google.golang.org/genproto v0.0.0-20250603155806-513f23925822 // indirect
|
google.golang.org/genproto v0.0.0-20250603155806-513f23925822 // indirect
|
||||||
google.golang.org/genproto/googleapis/api v0.0.0-20250818200422-3122310a409c // indirect
|
google.golang.org/genproto/googleapis/api v0.0.0-20250818200422-3122310a409c // indirect
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250818200422-3122310a409c // indirect
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20250818200422-3122310a409c // indirect
|
||||||
google.golang.org/grpc v1.74.2 // indirect
|
google.golang.org/grpc v1.74.2 // indirect
|
||||||
google.golang.org/protobuf v1.36.9 // indirect
|
google.golang.org/protobuf v1.36.11 // indirect
|
||||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
DROP TABLE IF EXISTS users;
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
CREATE TABLE users
|
||||||
|
(
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
telegram_id BIGINT UNIQUE NOT NULL,
|
||||||
|
username TEXT,
|
||||||
|
first_name TEXT NOT NULL,
|
||||||
|
last_name TEXT,
|
||||||
|
language_code TEXT,
|
||||||
|
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
|
||||||
|
updated_at TIMESTAMP NOT NULL DEFAULT NOW()
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX idx_users_telegram_id ON users (telegram_id);
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
DROP TABLE IF EXISTS telegram_chats;
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
CREATE TABLE telegram_chats
|
||||||
|
(
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
telegram_id BIGINT UNIQUE NOT NULL,
|
||||||
|
title TEXT NOT NULL,
|
||||||
|
created_at TIMESTAMP NOT NULL DEFAULT NOW()
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX idx_telegram_chats_telegram_id ON telegram_chats (telegram_id);
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
DROP INDEX idx_positions_receipt_id;
|
|
||||||
DROP INDEX idx_receipts_issued_at;
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
CREATE INDEX idx_receipts_issued_at ON receipts(issued_at);
|
|
||||||
CREATE INDEX idx_positions_receipt_id ON positions(receipt_id);
|
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
DROP TABLE IF EXISTS families;
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
CREATE TABLE families
|
||||||
|
(
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
name TEXT NOT NULL,
|
||||||
|
owner_id BIGINT NOT NULL REFERENCES users (id),
|
||||||
|
telegram_chat_id BIGINT NOT NULL UNIQUE REFERENCES telegram_chats (id),
|
||||||
|
created_at TIMESTAMP NOT NULL DEFAULT NOW()
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX idx_families_owner_id ON families (owner_id);
|
||||||
|
|
||||||
|
CREATE UNIQUE INDEX idx_families_chat_id ON families (telegram_chat_id);
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
DROP TABLE IF EXISTS family_members;
|
||||||
|
DROP TYPE IF EXISTS family_role;
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
CREATE TYPE family_role AS ENUM (
|
||||||
|
'owner',
|
||||||
|
'admin',
|
||||||
|
'member',
|
||||||
|
'child'
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE family_members
|
||||||
|
(
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
family_id BIGINT NOT NULL REFERENCES families (id) ON DELETE CASCADE,
|
||||||
|
user_id BIGINT NOT NULL REFERENCES users (id) ON DELETE CASCADE,
|
||||||
|
role family_role NOT NULL DEFAULT 'member',
|
||||||
|
joined_at TIMESTAMP NOT NULL DEFAULT NOW(),
|
||||||
|
UNIQUE (family_id, user_id)
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
-- быстрый поиск всех членов семьи
|
||||||
|
CREATE INDEX idx_family_members_family_id ON family_members (family_id);
|
||||||
|
|
||||||
|
-- быстрый поиск всех семей пользователя
|
||||||
|
CREATE INDEX idx_family_members_user_id ON family_members (user_id);
|
||||||
|
|
||||||
|
-- composite для частых join’ов
|
||||||
|
CREATE INDEX idx_family_members_user_family ON family_members (user_id, family_id);
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
DROP TABLE IF EXISTS threads;
|
||||||
|
DROP TYPE IF EXISTS thread_type;
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
CREATE TYPE thread_type AS ENUM (
|
||||||
|
'expenses',
|
||||||
|
'movies',
|
||||||
|
'schedule',
|
||||||
|
'recipes',
|
||||||
|
'custom'
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE threads
|
||||||
|
(
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
family_id BIGINT NOT NULL REFERENCES families (id) ON DELETE CASCADE,
|
||||||
|
type thread_type 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 threads (family_id, type) WHERE is_system = TRUE;
|
||||||
|
|
||||||
|
CREATE INDEX idx_threads_family_id ON threads (family_id);
|
||||||
|
|
||||||
|
CREATE INDEX idx_threads_family_type ON threads(family_id, type);
|
||||||
+4
-21
@@ -1,52 +1,35 @@
|
|||||||
CREATE TABLE receipts
|
CREATE TABLE receipts
|
||||||
(
|
(
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
|
||||||
-- основные поля
|
|
||||||
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,
|
||||||
|
|
||||||
issued_at TIMESTAMP NOT NULL,
|
issued_at TIMESTAMP NOT NULL,
|
||||||
|
|
||||||
-- суммы
|
|
||||||
total_amount REAL NOT NULL,
|
total_amount REAL NOT NULL,
|
||||||
payment_amount REAL NOT NULL,
|
payment_amount REAL NOT NULL,
|
||||||
cash_amount REAL NOT NULL,
|
cash_amount REAL NOT NULL,
|
||||||
another_amount REAL NOT NULL,
|
another_amount REAL NOT NULL,
|
||||||
clearing_amount REAL NOT NULL,
|
clearing_amount REAL NOT NULL,
|
||||||
margin REAL NOT NULL,
|
margin REAL NOT NULL,
|
||||||
|
|
||||||
currency TEXT NOT NULL,
|
currency TEXT NOT NULL,
|
||||||
payment_type INTEGER NOT NULL,
|
payment_type INTEGER NOT NULL,
|
||||||
|
|
||||||
-- касса / продавец
|
|
||||||
cashbox_number INTEGER NOT NULL,
|
cashbox_number INTEGER NOT NULL,
|
||||||
cashier TEXT,
|
cashier TEXT,
|
||||||
|
|
||||||
-- организация / адрес
|
|
||||||
name_spd TEXT,
|
name_spd TEXT,
|
||||||
name_to TEXT,
|
name_to TEXT,
|
||||||
name_np TEXT,
|
name_np TEXT,
|
||||||
type_np TEXT,
|
type_np TEXT,
|
||||||
|
|
||||||
street_to TEXT,
|
street_to TEXT,
|
||||||
house_to TEXT,
|
house_to TEXT,
|
||||||
|
|
||||||
-- SOATO (nullable)
|
|
||||||
kod_soato TEXT,
|
kod_soato TEXT,
|
||||||
oblast_soato TEXT,
|
oblast_soato TEXT,
|
||||||
rayon_soato TEXT,
|
rayon_soato TEXT,
|
||||||
selsovet_soato TEXT,
|
selsovet_soato TEXT,
|
||||||
|
|
||||||
-- прочее
|
|
||||||
doc_num TEXT,
|
doc_num TEXT,
|
||||||
skno_number TEXT,
|
skno_number TEXT,
|
||||||
unp TEXT,
|
unp TEXT,
|
||||||
|
|
||||||
success TEXT,
|
success TEXT,
|
||||||
|
created_at TIMESTAMP DEFAULT NOW()
|
||||||
|
|
||||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
|
||||||
);
|
);
|
||||||
|
|
||||||
|
CREATE INDEX idx_receipts_issued_at ON receipts(issued_at);
|
||||||
+5
-11
@@ -1,25 +1,19 @@
|
|||||||
CREATE TABLE positions
|
CREATE TABLE positions
|
||||||
(
|
(
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
receipt_id BIGINT NOT NULL,
|
||||||
receipt_id INTEGER NOT NULL,
|
|
||||||
|
|
||||||
section_number TEXT,
|
section_number TEXT,
|
||||||
gtin_code TEXT,
|
gtin_code TEXT,
|
||||||
|
|
||||||
product_name TEXT NOT NULL,
|
product_name TEXT NOT NULL,
|
||||||
|
|
||||||
product_count REAL NOT NULL,
|
product_count REAL NOT NULL,
|
||||||
amount REAL NOT NULL,
|
amount REAL NOT NULL,
|
||||||
|
|
||||||
discount REAL,
|
discount REAL,
|
||||||
surcharge REAL,
|
surcharge REAL,
|
||||||
|
|
||||||
tag TEXT,
|
tag TEXT,
|
||||||
marking_code TEXT,
|
marking_code TEXT,
|
||||||
ukz_code TEXT,
|
ukz_code TEXT,
|
||||||
|
|
||||||
FOREIGN KEY (receipt_id)
|
FOREIGN KEY (receipt_id) REFERENCES receipts (id) ON DELETE CASCADE
|
||||||
REFERENCES receipts (id)
|
|
||||||
ON DELETE CASCADE
|
|
||||||
);
|
);
|
||||||
|
|
||||||
|
CREATE INDEX idx_positions_receipt_id ON positions (receipt_id);
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
{
|
|
||||||
"STATUS": 1,
|
|
||||||
"another_amount": 0,
|
|
||||||
"cash_amount": 0,
|
|
||||||
"cashbox_number": 119066664,
|
|
||||||
"cashier": "Замена Магазин 3",
|
|
||||||
"clearing_amount": 13.54,
|
|
||||||
"currency": "BYN",
|
|
||||||
"doc_num": "153896",
|
|
||||||
"house_to": "11, 3 этаж",
|
|
||||||
"issued_at": "21/01/2026, 21:15:20",
|
|
||||||
"kod_soato": "5000000000",
|
|
||||||
"margin": 0,
|
|
||||||
"name_np": "Минск",
|
|
||||||
"name_spd": "Общество с ограниченной ответственностью \"СМАРТОН\"",
|
|
||||||
"name_to": "\"Офистон Маркет\", г.Минск, ул.Петра Мстиславца, 11",
|
|
||||||
"oblast_soato": null,
|
|
||||||
"payment_amount": 13.54,
|
|
||||||
"payment_type": 1,
|
|
||||||
"positions": "[{\"section_number\": \"0\", \"gtin_code\": \"8801068922011\", \"product_count\": \"1.000\", \"amount\": \"7.60\", \"discount\": \"0.00\", \"surcharge\": \"0.00\", \"tag\": \"0\", \"marking_code\": \"None\", \"ukz_code\": \"None\", \"product_name\": \"\\u041d\\u0430\\u043f\\u0438\\u0442\\u043e\\u043a \\\"Samlip\\\" 230 \\u043c\\u043b., \\u0441\\u043e \\u0432\\u043a\\u0443\\u0441\\u043e\\u043c \\u043b\\u0438\\u0447\\u0438\"}, {\"section_number\": \"0\", \"gtin_code\": \"4606008517920\", \"product_count\": \"1.000\", \"amount\": \"5.94\", \"discount\": \"0.00\", \"surcharge\": \"0.00\", \"tag\": \"0\", \"marking_code\": \"None\", \"ukz_code\": \"None\", \"product_name\": \"\\u0421\\u0442\\u0438\\u043a\\u0435\\u0440\\u044b \\u0434/\\u0437\\u0430\\u043c\\u0435\\u0442\\u043e\\u043a \\u0431\\u0443\\u043c\\u0430\\u0436\\u043d\\u044b\\u0435 \\\"\\u0421\\u0435\\u0440\\u0434\\u0446\\u0435\\\" 80 \\u0448\\u0442., \\u0444\\u0438\\u0433\\u0443\\u0440\\u043d\"}]",
|
|
||||||
"rayon_soato": null,
|
|
||||||
"receipt_number": "C0AD964BD53AC59A0718D028",
|
|
||||||
"selsovet_soato": null,
|
|
||||||
"skno_number": "AVQ24170087307",
|
|
||||||
"street_to": "УЛ. ПЕТРА МСТИСЛАВЦА",
|
|
||||||
"success": "A check is correct",
|
|
||||||
"total_amount": 13.54,
|
|
||||||
"type_np": "г.",
|
|
||||||
"ui": "C0AD964BD53AC59A0718D028",
|
|
||||||
"unp": "190635842"
|
|
||||||
}
|
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
package handlers
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net/http"
|
|
||||||
|
|
||||||
"GoFinanceManager/src/api/dto"
|
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Hello GoDoc
|
|
||||||
// @Summary Say hello
|
|
||||||
// @Description Returns greeting
|
|
||||||
// @Tags hello
|
|
||||||
// @Accept JSON
|
|
||||||
// @Produce JSON
|
|
||||||
// @Param name query string true "User name"
|
|
||||||
// @Success 200 {object} dto.HelloResponse
|
|
||||||
// @Failure 400 {object} dto.ErrorResponse
|
|
||||||
// @Router /hello [get]
|
|
||||||
func Hello(c *gin.Context) {
|
|
||||||
var req dto.HelloRequest
|
|
||||||
|
|
||||||
// биндинг + валидация
|
|
||||||
if err := c.ShouldBindQuery(&req); err != nil {
|
|
||||||
c.JSON(http.StatusBadRequest, dto.ErrorResponse{
|
|
||||||
Message: err.Error(),
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
resp := dto.HelloResponse{
|
|
||||||
Message: "Hello " + req.Name,
|
|
||||||
}
|
|
||||||
|
|
||||||
c.JSON(http.StatusOK, resp)
|
|
||||||
}
|
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
package handlers
|
package handlers
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"GoFinanceManager/src/api/dto"
|
"FamilyHub/src/api/dto"
|
||||||
"GoFinanceManager/src/integrations/receiptService"
|
"FamilyHub/src/integrations/receiptService"
|
||||||
"GoFinanceManager/src/utils"
|
"FamilyHub/src/utils"
|
||||||
"context"
|
"context"
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
@@ -20,30 +20,26 @@ func NewReceiptHandler(s *receiptService.ReceiptService) *ReceiptHandler {
|
|||||||
return &ReceiptHandler{service: s}
|
return &ReceiptHandler{service: s}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *ReceiptHandler) AddReceipt(c *gin.Context) {
|
func (handler *ReceiptHandler) AddReceipt(context_ *gin.Context) {
|
||||||
var req dto.AddReceiptRequest
|
var req dto.AddReceiptRequest
|
||||||
if err := c.ShouldBindJSON(&req); err != nil {
|
if err := context_.ShouldBindJSON(&req); err != nil {
|
||||||
log.Println("bind error:", err)
|
log.Println("bind error:", err)
|
||||||
c.JSON(http.StatusBadRequest, dto.ErrorResponse{
|
context_.JSON(http.StatusBadRequest, dto.ErrorResponse{Message: err.Error()})
|
||||||
Message: err.Error(),
|
|
||||||
})
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
isoDate, err := utils.NormalizeDateToISO(req.Date)
|
isoDate, err := utils.NormalizeDateToISO(req.Date)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(400, gin.H{"error": "invalid date format"})
|
context_.JSON(400, gin.H{"error": "invalid date format"})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
receipt, err := h.service.GetReceipt(
|
|
||||||
ctx,
|
receipt, err := handler.service.GetReceipt(ctx, isoDate, req.Number)
|
||||||
isoDate,
|
|
||||||
req.Number,
|
|
||||||
)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(400, gin.H{"error": "Cant get receipt"})
|
context_.JSON(400, gin.H{"error": err.Error()})
|
||||||
log.Print(err.Error())
|
log.Printf("API error, %s", err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -52,5 +48,5 @@ func (h *ReceiptHandler) AddReceipt(c *gin.Context) {
|
|||||||
Number: receipt.ReceiptNumber,
|
Number: receipt.ReceiptNumber,
|
||||||
Date: receipt.IssuedAt,
|
Date: receipt.IssuedAt,
|
||||||
}
|
}
|
||||||
c.JSON(http.StatusOK, resp)
|
context_.JSON(http.StatusOK, resp)
|
||||||
}
|
}
|
||||||
|
|||||||
+11
-7
@@ -1,11 +1,11 @@
|
|||||||
package api
|
package api
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"GoFinanceManager/src/api/handlers"
|
"FamilyHub/src/api/handlers"
|
||||||
"GoFinanceManager/src/config"
|
"FamilyHub/src/config"
|
||||||
"GoFinanceManager/src/database"
|
"FamilyHub/src/database"
|
||||||
"GoFinanceManager/src/integrations/receiptService"
|
"FamilyHub/src/integrations/receiptService"
|
||||||
"GoFinanceManager/src/repositories"
|
"FamilyHub/src/repositories"
|
||||||
"context"
|
"context"
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
@@ -19,12 +19,16 @@ type Server struct {
|
|||||||
|
|
||||||
func NewServer(cfg config.Config) *Server {
|
func NewServer(cfg config.Config) *Server {
|
||||||
handler := gin.Default()
|
handler := gin.Default()
|
||||||
dbConn, err := database.Connect(cfg.DBConnectionString)
|
dbManager := &database.Database{
|
||||||
|
ConnectionString: cfg.DBConnectionString,
|
||||||
|
MigrationsPath: "file://migrations",
|
||||||
|
}
|
||||||
|
dbConn, err := dbManager.Connect()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := database.RunMigrations(dbConn); err != nil {
|
if err := dbManager.RunMigrations(dbConn); err != nil {
|
||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+3
-3
@@ -1,9 +1,9 @@
|
|||||||
package bot
|
package bot
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"GoFinanceManager/src/config"
|
"FamilyHub/src/config"
|
||||||
"GoFinanceManager/src/integrations/ocr"
|
"FamilyHub/src/integrations/ocr"
|
||||||
"GoFinanceManager/src/integrations/receiptApi"
|
"FamilyHub/src/integrations/receiptApi"
|
||||||
"context"
|
"context"
|
||||||
"log"
|
"log"
|
||||||
"time"
|
"time"
|
||||||
|
|||||||
+6
-5
@@ -1,8 +1,8 @@
|
|||||||
package bot
|
package bot
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"GoFinanceManager/src/integrations/receiptApi"
|
"FamilyHub/src/integrations/receiptApi"
|
||||||
"GoFinanceManager/src/utils"
|
"FamilyHub/src/utils"
|
||||||
"context"
|
"context"
|
||||||
"io"
|
"io"
|
||||||
"log"
|
"log"
|
||||||
@@ -13,7 +13,6 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func (bot *Bot) handleMessage(msg *tgbotapi.Message) {
|
func (bot *Bot) handleMessage(msg *tgbotapi.Message) {
|
||||||
println(msg.Text)
|
|
||||||
switch msg.Text {
|
switch msg.Text {
|
||||||
case "/start":
|
case "/start":
|
||||||
bot.handleStart(msg)
|
bot.handleStart(msg)
|
||||||
@@ -26,7 +25,6 @@ func (bot *Bot) handleMessage(msg *tgbotapi.Message) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (bot *Bot) handlePhoto(msg *tgbotapi.Message) {
|
func (bot *Bot) handlePhoto(msg *tgbotapi.Message) {
|
||||||
// Берём самое большое фото
|
|
||||||
photo := msg.Photo[len(msg.Photo)-1]
|
photo := msg.Photo[len(msg.Photo)-1]
|
||||||
|
|
||||||
file, err := bot.api.GetFile(tgbotapi.FileConfig{FileID: photo.FileID})
|
file, err := bot.api.GetFile(tgbotapi.FileConfig{FileID: photo.FileID})
|
||||||
@@ -68,9 +66,12 @@ func (bot *Bot) handlePhoto(msg *tgbotapi.Message) {
|
|||||||
Date: receiptMeta.Date,
|
Date: receiptMeta.Date,
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 40*time.Second)
|
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
|
txt, err := utils.DecodeQR(imageBytes)
|
||||||
|
println(txt)
|
||||||
|
|
||||||
err = bot.receiptApi.SendReceipt(ctx, payload)
|
err = bot.receiptApi.SendReceipt(ctx, payload)
|
||||||
|
|
||||||
reply := "📄 *Результат распознавания*\n\n"
|
reply := "📄 *Результат распознавания*\n\n"
|
||||||
|
|||||||
+102
-7
@@ -2,22 +2,117 @@ package database
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"database/sql"
|
"database/sql"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/url"
|
"net/url"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
|
"github.com/golang-migrate/migrate/v4"
|
||||||
|
"github.com/golang-migrate/migrate/v4/database/postgres"
|
||||||
|
"github.com/golang-migrate/migrate/v4/database/sqlite3"
|
||||||
|
_ "github.com/golang-migrate/migrate/v4/source/file"
|
||||||
|
|
||||||
|
_ "github.com/lib/pq"
|
||||||
_ "github.com/mattn/go-sqlite3"
|
_ "github.com/mattn/go-sqlite3"
|
||||||
)
|
)
|
||||||
|
|
||||||
func Connect(dsn string) (*sql.DB, error) {
|
type Database struct {
|
||||||
u, err := url.Parse(dsn)
|
ConnectionString string
|
||||||
if err != nil {
|
MigrationsPath string
|
||||||
return nil, err
|
MaxOpenConns int
|
||||||
|
MaxIdleConns int
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Database) Connect() (*sql.DB, error) {
|
||||||
|
u, _ := url.Parse(d.ConnectionString)
|
||||||
|
if u == nil {
|
||||||
|
return nil, errors.New("nil url")
|
||||||
}
|
}
|
||||||
|
|
||||||
switch u.Scheme {
|
switch u.Scheme {
|
||||||
case "sqlite":
|
case "sqlite", "sqlite3":
|
||||||
return connectSQLite(u)
|
path := filepath.Join(u.Host, u.Path)
|
||||||
|
if path == "" {
|
||||||
|
return nil, fmt.Errorf("empty sqlite path")
|
||||||
|
}
|
||||||
|
|
||||||
|
dir := filepath.Dir(path)
|
||||||
|
if err := os.MkdirAll(dir, 0755); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
db, err := sql.Open("sqlite3", path)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if d.MaxOpenConns == 0 {
|
||||||
|
d.MaxOpenConns = 1
|
||||||
|
}
|
||||||
|
db.SetMaxOpenConns(d.MaxOpenConns)
|
||||||
|
return db, nil
|
||||||
|
|
||||||
|
case "postgres", "postgresql":
|
||||||
|
db, err := sql.Open("postgres", u.String())
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if d.MaxOpenConns == 0 {
|
||||||
|
d.MaxOpenConns = 20
|
||||||
|
}
|
||||||
|
if d.MaxIdleConns == 0 {
|
||||||
|
d.MaxIdleConns = 10
|
||||||
|
}
|
||||||
|
|
||||||
|
db.SetMaxOpenConns(d.MaxOpenConns)
|
||||||
|
db.SetMaxIdleConns(d.MaxIdleConns)
|
||||||
|
return db, nil
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return nil, fmt.Errorf("unsupported db scheme: %s", u.Scheme)
|
return nil, fmt.Errorf("unsupported database scheme: %s", u.Scheme)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (d *Database) RunMigrations(db *sql.DB) error {
|
||||||
|
u, _ := url.Parse(d.ConnectionString)
|
||||||
|
if u == nil {
|
||||||
|
return errors.New("nil url")
|
||||||
|
}
|
||||||
|
|
||||||
|
if db == nil {
|
||||||
|
return errors.New("nil db")
|
||||||
|
}
|
||||||
|
|
||||||
|
if d.MigrationsPath == "" {
|
||||||
|
d.MigrationsPath = "file://migrations"
|
||||||
|
}
|
||||||
|
|
||||||
|
var m *migrate.Migrate
|
||||||
|
|
||||||
|
switch u.Scheme {
|
||||||
|
case "sqlite", "sqlite3":
|
||||||
|
driver, err := sqlite3.WithInstance(db, &sqlite3.Config{})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
m, err = migrate.NewWithDatabaseInstance(d.MigrationsPath, "sqlite", driver)
|
||||||
|
|
||||||
|
case "postgres", "postgresql":
|
||||||
|
driver, err := postgres.WithInstance(db, &postgres.Config{})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
m, err = migrate.NewWithDatabaseInstance(d.MigrationsPath, "postgres", driver)
|
||||||
|
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("unsupported database scheme for migrations: %s", u.Scheme)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := m.Up(); err != nil && !errors.Is(err, migrate.ErrNoChange) {
|
||||||
|
return fmt.Errorf("migration failed: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,32 +0,0 @@
|
|||||||
package database
|
|
||||||
|
|
||||||
import (
|
|
||||||
"database/sql"
|
|
||||||
"errors"
|
|
||||||
|
|
||||||
"github.com/golang-migrate/migrate/v4"
|
|
||||||
"github.com/golang-migrate/migrate/v4/database/sqlite3"
|
|
||||||
_ "github.com/golang-migrate/migrate/v4/source/file"
|
|
||||||
)
|
|
||||||
|
|
||||||
func RunMigrations(db *sql.DB) error {
|
|
||||||
driver, err := sqlite3.WithInstance(db, &sqlite3.Config{})
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
m, err := migrate.NewWithDatabaseInstance(
|
|
||||||
"file://migrations",
|
|
||||||
"sqlite",
|
|
||||||
driver,
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := m.Up(); err != nil && !errors.Is(err, migrate.ErrNoChange) {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
package database
|
||||||
|
|
||||||
|
import (
|
||||||
|
"database/sql"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"net/url"
|
||||||
|
|
||||||
|
"github.com/golang-migrate/migrate/v4"
|
||||||
|
"github.com/golang-migrate/migrate/v4/database/postgres"
|
||||||
|
_ "github.com/golang-migrate/migrate/v4/source/file"
|
||||||
|
)
|
||||||
|
|
||||||
|
type PostgresDB struct {
|
||||||
|
MigrationsPath string
|
||||||
|
MaxOpenConns int
|
||||||
|
MaxIdleConns int
|
||||||
|
}
|
||||||
|
|
||||||
|
// Connect открывает соединение с Postgres
|
||||||
|
func (p *PostgresDB) Connect(u *url.URL) (*sql.DB, error) {
|
||||||
|
db, err := sql.Open("postgres", u.String())
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if p.MaxOpenConns == 0 {
|
||||||
|
p.MaxOpenConns = 20
|
||||||
|
}
|
||||||
|
if p.MaxIdleConns == 0 {
|
||||||
|
p.MaxIdleConns = 10
|
||||||
|
}
|
||||||
|
|
||||||
|
db.SetMaxOpenConns(p.MaxOpenConns)
|
||||||
|
db.SetMaxIdleConns(p.MaxIdleConns)
|
||||||
|
|
||||||
|
return db, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// RunMigrations запускает миграции PostgreSQL
|
||||||
|
func (p *PostgresDB) RunMigrations(db *sql.DB) error {
|
||||||
|
driver, err := postgres.WithInstance(db, &postgres.Config{})
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to create postgres driver: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if p.MigrationsPath == "" {
|
||||||
|
p.MigrationsPath = "file://migrations"
|
||||||
|
}
|
||||||
|
|
||||||
|
m, err := migrate.NewWithDatabaseInstance(
|
||||||
|
p.MigrationsPath,
|
||||||
|
"postgres",
|
||||||
|
driver,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to create migrate instance: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := m.Up(); err != nil && !errors.Is(err, migrate.ErrNoChange) {
|
||||||
|
return fmt.Errorf("migration failed: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
+41
-2
@@ -2,13 +2,23 @@ package database
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"database/sql"
|
"database/sql"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
|
||||||
|
"github.com/golang-migrate/migrate/v4"
|
||||||
|
"github.com/golang-migrate/migrate/v4/database/sqlite3"
|
||||||
|
_ "github.com/golang-migrate/migrate/v4/source/file"
|
||||||
)
|
)
|
||||||
|
|
||||||
func connectSQLite(u *url.URL) (*sql.DB, error) {
|
type SQLiteDB struct {
|
||||||
|
MigrationsPath string
|
||||||
|
MaxOpenConns int
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SQLiteDB) Connect(u *url.URL) (*sql.DB, error) {
|
||||||
path := filepath.Join(u.Host, u.Path)
|
path := filepath.Join(u.Host, u.Path)
|
||||||
if path == "" {
|
if path == "" {
|
||||||
return nil, fmt.Errorf("empty sqlite path")
|
return nil, fmt.Errorf("empty sqlite path")
|
||||||
@@ -24,6 +34,35 @@ func connectSQLite(u *url.URL) (*sql.DB, error) {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
db.SetMaxOpenConns(1) // важно для sqlite
|
if s.MaxOpenConns == 0 {
|
||||||
|
s.MaxOpenConns = 1
|
||||||
|
}
|
||||||
|
db.SetMaxOpenConns(s.MaxOpenConns)
|
||||||
return db, nil
|
return db, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *SQLiteDB) RunMigrations(db *sql.DB) error {
|
||||||
|
driver, err := sqlite3.WithInstance(db, &sqlite3.Config{})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if s.MigrationsPath == "" {
|
||||||
|
s.MigrationsPath = "file://migrations"
|
||||||
|
}
|
||||||
|
|
||||||
|
m, err := migrate.NewWithDatabaseInstance(
|
||||||
|
s.MigrationsPath,
|
||||||
|
"sqlite",
|
||||||
|
driver,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := m.Up(); err != nil && !errors.Is(err, migrate.ErrNoChange) {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,67 @@
|
|||||||
|
package models
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
|
type User struct {
|
||||||
|
ID int64
|
||||||
|
TelegramID int64
|
||||||
|
Username *string
|
||||||
|
FirstName string
|
||||||
|
LastName *string
|
||||||
|
LanguageCode *string
|
||||||
|
CreatedAt time.Time
|
||||||
|
UpdatedAt time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
type TelegramChat struct {
|
||||||
|
ID int64
|
||||||
|
TelegramID int64
|
||||||
|
Title string
|
||||||
|
CreatedAt time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
type Family struct {
|
||||||
|
ID int64
|
||||||
|
Name string
|
||||||
|
OwnerID int64
|
||||||
|
TelegramChatID int64
|
||||||
|
CreatedAt time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
type FamilyRole string
|
||||||
|
|
||||||
|
const (
|
||||||
|
FamilyRoleOwner FamilyRole = "owner"
|
||||||
|
FamilyRoleAdmin FamilyRole = "admin"
|
||||||
|
FamilyRoleMember FamilyRole = "member"
|
||||||
|
FamilyRoleChild FamilyRole = "child"
|
||||||
|
)
|
||||||
|
|
||||||
|
type FamilyMember struct {
|
||||||
|
ID int64
|
||||||
|
FamilyID int64
|
||||||
|
UserID int64
|
||||||
|
Role FamilyRole
|
||||||
|
JoinedAt time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
type ThreadType string
|
||||||
|
|
||||||
|
const (
|
||||||
|
ThreadExpenses ThreadType = "expenses"
|
||||||
|
ThreadMovies ThreadType = "movies"
|
||||||
|
ThreadSchedule ThreadType = "schedule"
|
||||||
|
ThreadRecipes ThreadType = "recipes"
|
||||||
|
ThreadCustom ThreadType = "custom"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Thread struct {
|
||||||
|
ID int64
|
||||||
|
FamilyID int64
|
||||||
|
Type ThreadType
|
||||||
|
Title string
|
||||||
|
TelegramTopicID int64
|
||||||
|
IsSystem bool
|
||||||
|
CreatedBy int64
|
||||||
|
CreatedAt time.Time
|
||||||
|
}
|
||||||
@@ -19,7 +19,7 @@ func NewHTTPClient(baseURL string) (*HTTPClient, error) {
|
|||||||
return &HTTPClient{
|
return &HTTPClient{
|
||||||
baseURL: baseURL,
|
baseURL: baseURL,
|
||||||
client: &http.Client{
|
client: &http.Client{
|
||||||
Timeout: 30 * time.Second,
|
Timeout: 60 * time.Second,
|
||||||
},
|
},
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
@@ -44,9 +44,6 @@ func (c *HTTPClient) SendReceipt(
|
|||||||
}
|
}
|
||||||
|
|
||||||
req.Header.Set("Content-Type", "application/json")
|
req.Header.Set("Content-Type", "application/json")
|
||||||
//if c.apiKey != "" {
|
|
||||||
// req.Header.Set("Authorization", "Bearer "+c.apiKey)
|
|
||||||
//}
|
|
||||||
|
|
||||||
resp, err := c.client.Do(req)
|
resp, err := c.client.Do(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -1,13 +1,14 @@
|
|||||||
package receiptService
|
package receiptService
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"GoFinanceManager/src/domain/models"
|
"FamilyHub/src/domain/models"
|
||||||
"GoFinanceManager/src/repositories"
|
"FamilyHub/src/repositories"
|
||||||
"GoFinanceManager/src/utils"
|
"FamilyHub/src/utils"
|
||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
"mime/multipart"
|
"mime/multipart"
|
||||||
@@ -23,7 +24,7 @@ type ReceiptService struct {
|
|||||||
func NewReceiptService(repo repositories.ReceiptRepository) *ReceiptService {
|
func NewReceiptService(repo repositories.ReceiptRepository) *ReceiptService {
|
||||||
return &ReceiptService{
|
return &ReceiptService{
|
||||||
client: &http.Client{
|
client: &http.Client{
|
||||||
Timeout: 30 * time.Second,
|
Timeout: 60 * time.Second,
|
||||||
Transport: &http.Transport{
|
Transport: &http.Transport{
|
||||||
TLSClientConfig: &tls.Config{
|
TLSClientConfig: &tls.Config{
|
||||||
InsecureSkipVerify: true,
|
InsecureSkipVerify: true,
|
||||||
@@ -40,8 +41,8 @@ func (s *ReceiptService) GetReceipt(
|
|||||||
number string,
|
number string,
|
||||||
) (*models.Receipt, error) {
|
) (*models.Receipt, error) {
|
||||||
url := "https://ch.info-center.by/ajax/check1.php"
|
url := "https://ch.info-center.by/ajax/check1.php"
|
||||||
|
|
||||||
var receipt models.Receipt
|
var receipt models.Receipt
|
||||||
|
|
||||||
body, contentType := buildMultipartBody(date, number)
|
body, contentType := buildMultipartBody(date, number)
|
||||||
req, err := http.NewRequestWithContext(
|
req, err := http.NewRequestWithContext(
|
||||||
ctx,
|
ctx,
|
||||||
@@ -71,26 +72,26 @@ func (s *ReceiptService) GetReceipt(
|
|||||||
var raw struct {
|
var raw struct {
|
||||||
Message map[string]interface{} `json:"message"`
|
Message map[string]interface{} `json:"message"`
|
||||||
}
|
}
|
||||||
log.Println(raw.Message)
|
|
||||||
if err := json.NewDecoder(resp.Body).Decode(&raw); err != nil {
|
if err := json.NewDecoder(resp.Body).Decode(&raw); err != nil {
|
||||||
log.Printf("external service returned %s\n", err.Error())
|
log.Printf("external service returned %s\n", err.Error())
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
raw.Message["receipt_number"] = number
|
|
||||||
|
|
||||||
bytes_, _ := json.Marshal(raw.Message)
|
bytes_, _ := json.Marshal(raw.Message)
|
||||||
|
|
||||||
if err := json.Unmarshal(bytes_, &receipt); err != nil {
|
if err := json.Unmarshal(bytes_, &receipt); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
log.Println(receipt)
|
|
||||||
|
if receipt.IssuedAtRaw == "" {
|
||||||
|
return nil, errors.New("receipt not found")
|
||||||
|
}
|
||||||
|
|
||||||
positions, err := parsePositions(receipt.PositionsRaw)
|
positions, err := parsePositions(receipt.PositionsRaw)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("failed to parse positions: %s", err.Error())
|
log.Printf("failed to parse positions: %s", err.Error())
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
log.Println(receipt.IssuedAtRaw)
|
|
||||||
receipt.IssuedAt, err = utils.ParseIssuedAt(receipt.IssuedAtRaw)
|
receipt.IssuedAt, err = utils.ParseIssuedAt(receipt.IssuedAtRaw)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("failed to parse issued at: %s", err.Error())
|
log.Printf("failed to parse issued at: %s", err.Error())
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"GoFinanceManager/src/api"
|
"FamilyHub/src/api"
|
||||||
"GoFinanceManager/src/bot"
|
"FamilyHub/src/bot"
|
||||||
"GoFinanceManager/src/config"
|
"FamilyHub/src/config"
|
||||||
|
|
||||||
"context"
|
"context"
|
||||||
"log"
|
"log"
|
||||||
@@ -3,7 +3,7 @@ package repositories
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
|
||||||
"GoFinanceManager/src/domain/models"
|
"FamilyHub/src/domain/models"
|
||||||
)
|
)
|
||||||
|
|
||||||
type ReceiptRepository interface {
|
type ReceiptRepository interface {
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import (
|
|||||||
"database/sql"
|
"database/sql"
|
||||||
"errors"
|
"errors"
|
||||||
|
|
||||||
"GoFinanceManager/src/domain/models"
|
"FamilyHub/src/domain/models"
|
||||||
)
|
)
|
||||||
|
|
||||||
type ReceiptSQLRepository struct {
|
type ReceiptSQLRepository struct {
|
||||||
@@ -16,17 +16,16 @@ func NewReceiptSQLRepository(db *sql.DB) *ReceiptSQLRepository {
|
|||||||
return &ReceiptSQLRepository{db: db}
|
return &ReceiptSQLRepository{db: db}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *ReceiptSQLRepository) Create(
|
func (r *ReceiptSQLRepository) Create(ctx context.Context, receipt *models.Receipt) (int64, error) {
|
||||||
ctx context.Context,
|
|
||||||
receipt *models.Receipt,
|
|
||||||
) (int64, error) {
|
|
||||||
|
|
||||||
tx, err := r.db.BeginTx(ctx, nil)
|
tx, err := r.db.BeginTx(ctx, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, err
|
return 0, err
|
||||||
}
|
}
|
||||||
defer tx.Rollback()
|
defer tx.Rollback()
|
||||||
|
if receipt.ReceiptNumber != receipt.UI {
|
||||||
|
receipt.ReceiptNumber = receipt.UI
|
||||||
|
}
|
||||||
res, err := tx.ExecContext(ctx, `
|
res, err := tx.ExecContext(ctx, `
|
||||||
INSERT INTO receipts (
|
INSERT INTO receipts (
|
||||||
receipt_number, ui, status, issued_at,
|
receipt_number, ui, status, issued_at,
|
||||||
@@ -130,10 +129,7 @@ func (r *ReceiptSQLRepository) Create(
|
|||||||
return receiptID, tx.Commit()
|
return receiptID, tx.Commit()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *ReceiptSQLRepository) GetByID(
|
func (r *ReceiptSQLRepository) GetByID(ctx context.Context, id int64) (*models.Receipt, error) {
|
||||||
ctx context.Context,
|
|
||||||
id int64,
|
|
||||||
) (*models.Receipt, error) {
|
|
||||||
|
|
||||||
var receipt models.Receipt
|
var receipt models.Receipt
|
||||||
|
|
||||||
@@ -234,10 +230,7 @@ func (r *ReceiptSQLRepository) GetByID(
|
|||||||
return &receipt, nil
|
return &receipt, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *ReceiptSQLRepository) GetAll(
|
func (r *ReceiptSQLRepository) GetAll(ctx context.Context, limit, offset int) ([]*models.Receipt, error) {
|
||||||
ctx context.Context,
|
|
||||||
limit, offset int,
|
|
||||||
) ([]*models.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, receipt_number, issued_at, total_amount, currency
|
||||||
@@ -269,10 +262,7 @@ func (r *ReceiptSQLRepository) GetAll(
|
|||||||
return receipts, nil
|
return receipts, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *ReceiptSQLRepository) Update(
|
func (r *ReceiptSQLRepository) Update(ctx context.Context, receipt *models.Receipt) error {
|
||||||
ctx context.Context,
|
|
||||||
receipt *models.Receipt,
|
|
||||||
) error {
|
|
||||||
|
|
||||||
tx, err := r.db.BeginTx(ctx, nil)
|
tx, err := r.db.BeginTx(ctx, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -315,10 +305,7 @@ func (r *ReceiptSQLRepository) Update(
|
|||||||
return tx.Commit()
|
return tx.Commit()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *ReceiptSQLRepository) Delete(
|
func (r *ReceiptSQLRepository) Delete(ctx context.Context, id int64) error {
|
||||||
ctx context.Context,
|
|
||||||
id int64,
|
|
||||||
) error {
|
|
||||||
_, err := r.db.ExecContext(ctx,
|
_, err := r.db.ExecContext(ctx,
|
||||||
`DELETE FROM receipts WHERE id = ?`,
|
`DELETE FROM receipts WHERE id = ?`,
|
||||||
id,
|
id,
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ package utils
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"log"
|
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
@@ -21,7 +20,6 @@ var knownDateFormats = []string{
|
|||||||
|
|
||||||
func NormalizeDateToISO(input string) (string, error) {
|
func NormalizeDateToISO(input string) (string, error) {
|
||||||
input = strings.TrimSpace(input)
|
input = strings.TrimSpace(input)
|
||||||
log.Println(input)
|
|
||||||
for _, layout := range knownDateFormats {
|
for _, layout := range knownDateFormats {
|
||||||
if t, err := time.Parse(layout, input); err == nil {
|
if t, err := time.Parse(layout, input); err == nil {
|
||||||
return t.Format("2006-01-02"), nil
|
return t.Format("2006-01-02"), nil
|
||||||
|
|||||||
@@ -0,0 +1,30 @@
|
|||||||
|
package utils
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"image"
|
||||||
|
_ "image/jpeg"
|
||||||
|
_ "image/png"
|
||||||
|
|
||||||
|
"github.com/liyue201/goqr"
|
||||||
|
)
|
||||||
|
|
||||||
|
func DecodeQR(imageBytes []byte) ([]string, error) {
|
||||||
|
img, _, err := image.Decode(bytes.NewReader(imageBytes))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
codes, err := goqr.Recognize(img)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
println(codes)
|
||||||
|
|
||||||
|
var result []string
|
||||||
|
for _, code := range codes {
|
||||||
|
result = append(result, string(code.Payload))
|
||||||
|
}
|
||||||
|
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user