diff --git a/cmd/smm_core/main.go b/cmd/smm_core/main.go index 2b492fe..59650ec 100644 --- a/cmd/smm_core/main.go +++ b/cmd/smm_core/main.go @@ -9,6 +9,7 @@ import ( "os" "git.3crabs.ru/save_my_money/smm_core/internal/app" + "git.3crabs.ru/save_my_money/smm_core/internal/services/budget" "git.3crabs.ru/save_my_money/smm_core/internal/services/category" "git.3crabs.ru/save_my_money/smm_core/internal/services/user" "github.com/grpc-ecosystem/grpc-gateway/v2/runtime" @@ -38,6 +39,7 @@ func main() { categoryService := category.NewCategoryService(dbpool) userService := user.NewUserService(dbpool) + budgetService := budget.NewBudgetService(dbpool) // Create a gRPC server object s := grpc.NewServer( @@ -61,7 +63,14 @@ func main() { ), ) // Attach the Greeter service to the server - proto.RegisterSmmCoreServer(s, app.NewServer(categoryService, userService)) + proto.RegisterSmmCoreServer( + s, + app.NewServer( + categoryService, + userService, + budgetService, + ), + ) // Serve gRPC server log.Println("Serving gRPC on 0.0.0.0:8080") go func() { diff --git a/internal/app/server.go b/internal/app/server.go index 7c04c2b..b921f5d 100644 --- a/internal/app/server.go +++ b/internal/app/server.go @@ -3,6 +3,7 @@ package app import ( "context" + "git.3crabs.ru/save_my_money/smm_core/internal/services/budget" "git.3crabs.ru/save_my_money/smm_core/internal/services/category" "git.3crabs.ru/save_my_money/smm_core/internal/services/user" proto "git.3crabs.ru/save_my_money/smm_core/proto" @@ -12,15 +13,18 @@ type Server struct { proto.UnsafeSmmCoreServer categoryService *category.CategoryService userService *user.UserService + budgetService *budget.BudgetService } func NewServer( categoryService *category.CategoryService, userService *user.UserService, + budgetService *budget.BudgetService, ) proto.SmmCoreServer { return &Server{ categoryService: categoryService, userService: userService, + budgetService: budgetService, } } @@ -62,9 +66,24 @@ func (s *Server) Login(ctx context.Context, req *proto.LoginReq) (*proto.User, e }, nil } -// AddBudget implements proto.SmmCoreServer. -func (s *Server) AddBudget(context.Context, *proto.AddBudgetReq) (*proto.Budget, error) { - panic("unimplemented") +func (s *Server) AddBudget(ctx context.Context, req *proto.AddBudgetReq) (*proto.Budget, error) { + budget, err := s.budgetService.AddBudget( + ctx, + &budget.BudgetEntity{ + Name: req.Name, + StartDay: int(req.StartDay), + MonthlyLimit: int(req.MonthlyLimit), + }, + ) + if err != nil { + return nil, err + } + return &proto.Budget{ + Id: int32(budget.Id), + Name: budget.Name, + StartDay: int32(budget.StartDay), + MonthlyLimit: int32(budget.MonthlyLimit), + }, nil } // AddCategory implements proto.SmmCoreServer. diff --git a/internal/services/budget/budget_service.go b/internal/services/budget/budget_service.go new file mode 100644 index 0000000..4fc660f --- /dev/null +++ b/internal/services/budget/budget_service.go @@ -0,0 +1,66 @@ +package budget + +import ( + "context" + "fmt" + + "git.3crabs.ru/save_my_money/smm_core/internal/services/context_utils" + "github.com/jackc/pgx/v5" + "github.com/jackc/pgx/v5/pgxpool" +) + +type BudgetEntity struct { + Id int + Name string + StartDay int + MonthlyLimit int +} + +type BudgetService struct { + db *pgxpool.Pool +} + +func NewBudgetService( + db *pgxpool.Pool, +) *BudgetService { + return &BudgetService{ + db: db, + } +} + +func (s *BudgetService) AddBudget(ctx context.Context, budget *BudgetEntity) (*BudgetEntity, error) { + userId, err := context_utils.GetUserId(ctx) + if err != nil { + return nil, err + } + + tx, err := s.db.Begin(ctx) + if err != nil { + return nil, err + } + defer tx.Rollback(ctx) + + query := `INSERT INTO budgets (name, start_day, monthly_limit) VALUES (@name, @start_day, @monthly_limit) RETURNING id` + args := pgx.NamedArgs{ + "name": budget.Name, + "start_day": budget.StartDay, + "monthly_limit": budget.MonthlyLimit, + } + if err := s.db.QueryRow(ctx, query, args).Scan(&budget.Id); err != nil { + return nil, fmt.Errorf("unable to insert row: %w", err) + } + + query = `INSERT INTO users_budgets (user_id, budget_id) VALUES (@user_id, @budget_id) RETURNING id` + args = pgx.NamedArgs{ + "user_id": userId, + "budget_id": budget.Id, + } + if err := s.db.QueryRow(ctx, query, args).Scan(&budget.Id); err != nil { + return nil, fmt.Errorf("unable to insert row: %w", err) + } + + if err = tx.Commit(ctx); err != nil { + return nil, err + } + return budget, nil +} diff --git a/internal/services/requests.restbook b/internal/services/requests.restbook index 5f86a55..168f936 100644 --- a/internal/services/requests.restbook +++ b/internal/services/requests.restbook @@ -1 +1 @@ -[{"kind":1,"language":"markdown","value":"# Добавление пользователя","outputs":[]},{"kind":2,"language":"rest-book","value":"POST http://localhost:8090/users\nauthorization: Y3JhYjpjcmFi\n\n{\n \"username\": \"foo\",\n \"password\": \"bar\"\n}","outputs":[{"mime":"x-application/rest-book","value":{"status":200,"statusText":"OK","headers":{"Date":"Thu, 21 Nov 2024 09:02:06 GMT","Content-Type":"application/json","Content-Length":"27"},"config":{"timeout":10000,"xsrfCookieName":"XSRF-TOKEN","xsrfHeaderName":"X-XSRF-TOKEN","headers":{"Accept":"application/json, text/plain, */*","Content-Type":"application/json","authorization":"Y3JhYjpjcmFi","User-Agent":"axios/0.21.4","Content-Length":36}},"request":{"method":"POST","httpVersion":"1.1","responseUrl":"http://localhost:8090/users","timeout":10000,"headers":{"authorization":"Y3JhYjpjcmFi"},"data":{"username":"foo3","password":"bar"}},"data":{"id":5,"username":"foo3"}}},{"mime":"text/x-json","value":{"status":200,"statusText":"OK","headers":{"Date":"Thu, 21 Nov 2024 09:02:06 GMT","Content-Type":"application/json","Content-Length":"27"},"config":{"timeout":10000,"xsrfCookieName":"XSRF-TOKEN","xsrfHeaderName":"X-XSRF-TOKEN","headers":{"Accept":"application/json, text/plain, */*","Content-Type":"application/json","authorization":"Y3JhYjpjcmFi","User-Agent":"axios/0.21.4","Content-Length":36}},"request":{"method":"POST","httpVersion":"1.1","responseUrl":"http://localhost:8090/users","timeout":10000,"headers":{"authorization":"Y3JhYjpjcmFi"},"data":{"username":"foo3","password":"bar"}},"data":{"id":5,"username":"foo3"}}},{"mime":"text/html","value":"[object Object]"}]},{"kind":1,"language":"markdown","value":"# Авторизация","outputs":[]},{"kind":2,"language":"rest-book","value":"POST http://localhost:8090/login\nauthorization: Y3JhYjpjcmFi\n\n{\n \"username\": \"foo\",\n \"password\": \"bar\"\n}","outputs":[{"mime":"x-application/rest-book","value":{"status":200,"statusText":"OK","headers":{"Date":"Thu, 21 Nov 2024 09:02:13 GMT","Content-Type":"application/json","Content-Length":"26"},"config":{"timeout":10000,"xsrfCookieName":"XSRF-TOKEN","xsrfHeaderName":"X-XSRF-TOKEN","headers":{"Accept":"application/json, text/plain, */*","Content-Type":"application/json","authorization":"Y3JhYjpjcmFi","User-Agent":"axios/0.21.4","Content-Length":35}},"request":{"method":"POST","httpVersion":"1.1","responseUrl":"http://localhost:8090/login","timeout":10000,"headers":{"authorization":"Y3JhYjpjcmFi"},"data":{"username":"foo","password":"bar"}},"data":{"id":1,"username":"foo"}}},{"mime":"text/x-json","value":{"status":200,"statusText":"OK","headers":{"Date":"Thu, 21 Nov 2024 09:02:13 GMT","Content-Type":"application/json","Content-Length":"26"},"config":{"timeout":10000,"xsrfCookieName":"XSRF-TOKEN","xsrfHeaderName":"X-XSRF-TOKEN","headers":{"Accept":"application/json, text/plain, */*","Content-Type":"application/json","authorization":"Y3JhYjpjcmFi","User-Agent":"axios/0.21.4","Content-Length":35}},"request":{"method":"POST","httpVersion":"1.1","responseUrl":"http://localhost:8090/login","timeout":10000,"headers":{"authorization":"Y3JhYjpjcmFi"},"data":{"username":"foo","password":"bar"}},"data":{"id":1,"username":"foo"}}},{"mime":"text/html","value":"[object Object]"}]},{"kind":1,"language":"markdown","value":"# Добавление категории","outputs":[]},{"kind":2,"language":"rest-book","value":"POST http://localhost:8090/categories\nUser-Id: 1\n\n{\n \"name\": \"Продукты питания\"\n}","outputs":[{"mime":"x-application/rest-book","value":{"status":500,"statusText":"Internal Server Error","headers":{"Date":"Wed, 20 Nov 2024 16:25:26 GMT","Content-Type":"application/json","Content-Length":"184"},"config":{"timeout":10000,"xsrfCookieName":"XSRF-TOKEN","xsrfHeaderName":"X-XSRF-TOKEN","headers":{"Accept":"application/json, text/plain, */*","Content-Type":"application/json","User-Id":"2","User-Agent":"axios/0.21.4","Content-Length":42}},"request":{"method":"POST","httpVersion":"1.1","responseUrl":"http://localhost:8090/categories","timeout":10000,"headers":{"User-Id":"2"},"data":{"name":"Продукты питания"}},"data":{"code":2,"message":"unable to insert row: ERROR: insert or update on table \"categories\" violates foreign key constraint \"categories_user_id_fkey\" (SQLSTATE 23503)","details":[]}}},{"mime":"text/x-json","value":{"status":500,"statusText":"Internal Server Error","headers":{"Date":"Wed, 20 Nov 2024 16:25:26 GMT","Content-Type":"application/json","Content-Length":"184"},"config":{"timeout":10000,"xsrfCookieName":"XSRF-TOKEN","xsrfHeaderName":"X-XSRF-TOKEN","headers":{"Accept":"application/json, text/plain, */*","Content-Type":"application/json","User-Id":"2","User-Agent":"axios/0.21.4","Content-Length":42}},"request":{"method":"POST","httpVersion":"1.1","responseUrl":"http://localhost:8090/categories","timeout":10000,"headers":{"User-Id":"2"},"data":{"name":"Продукты питания"}},"data":{"code":2,"message":"unable to insert row: ERROR: insert or update on table \"categories\" violates foreign key constraint \"categories_user_id_fkey\" (SQLSTATE 23503)","details":[]}}},{"mime":"text/html","value":"[object Object]"}]}] \ No newline at end of file +[{"kind":1,"language":"markdown","value":"# Добавление пользователя","outputs":[]},{"kind":2,"language":"rest-book","value":"POST http://localhost:8090/users\nauthorization: Y3JhYjpjcmFi\n\n{\n \"username\": \"foo\",\n \"password\": \"bar\"\n}","outputs":[{"mime":"x-application/rest-book","value":{"status":200,"statusText":"OK","headers":{"Date":"Sun, 24 Nov 2024 08:59:31 GMT","Content-Type":"application/json","Content-Length":"25"},"config":{"timeout":10000,"xsrfCookieName":"XSRF-TOKEN","xsrfHeaderName":"X-XSRF-TOKEN","headers":{"Accept":"application/json, text/plain, */*","Content-Type":"application/json","authorization":"Y3JhYjpjcmFi","User-Agent":"axios/0.21.4","Content-Length":35}},"request":{"method":"POST","httpVersion":"1.1","responseUrl":"http://localhost:8090/users","timeout":10000,"headers":{"authorization":"Y3JhYjpjcmFi"},"data":{"username":"foo","password":"bar"}},"data":{"id":1,"username":"foo"}}},{"mime":"text/x-json","value":{"status":200,"statusText":"OK","headers":{"Date":"Sun, 24 Nov 2024 08:59:31 GMT","Content-Type":"application/json","Content-Length":"25"},"config":{"timeout":10000,"xsrfCookieName":"XSRF-TOKEN","xsrfHeaderName":"X-XSRF-TOKEN","headers":{"Accept":"application/json, text/plain, */*","Content-Type":"application/json","authorization":"Y3JhYjpjcmFi","User-Agent":"axios/0.21.4","Content-Length":35}},"request":{"method":"POST","httpVersion":"1.1","responseUrl":"http://localhost:8090/users","timeout":10000,"headers":{"authorization":"Y3JhYjpjcmFi"},"data":{"username":"foo","password":"bar"}},"data":{"id":1,"username":"foo"}}},{"mime":"text/html","value":"[object Object]"}]},{"kind":1,"language":"markdown","value":"# Авторизация","outputs":[]},{"kind":2,"language":"rest-book","value":"POST http://localhost:8090/login\nauthorization: Y3JhYjpjcmFi\n\n{\n \"username\": \"foo\",\n \"password\": \"bar\"\n}","outputs":[{"mime":"x-application/rest-book","value":{"status":200,"statusText":"OK","headers":{"Date":"Sun, 24 Nov 2024 08:59:35 GMT","Content-Type":"application/json","Content-Length":"25"},"config":{"timeout":10000,"xsrfCookieName":"XSRF-TOKEN","xsrfHeaderName":"X-XSRF-TOKEN","headers":{"Accept":"application/json, text/plain, */*","Content-Type":"application/json","authorization":"Y3JhYjpjcmFi","User-Agent":"axios/0.21.4","Content-Length":35}},"request":{"method":"POST","httpVersion":"1.1","responseUrl":"http://localhost:8090/login","timeout":10000,"headers":{"authorization":"Y3JhYjpjcmFi"},"data":{"username":"foo","password":"bar"}},"data":{"id":1,"username":"foo"}}},{"mime":"text/x-json","value":{"status":200,"statusText":"OK","headers":{"Date":"Sun, 24 Nov 2024 08:59:35 GMT","Content-Type":"application/json","Content-Length":"25"},"config":{"timeout":10000,"xsrfCookieName":"XSRF-TOKEN","xsrfHeaderName":"X-XSRF-TOKEN","headers":{"Accept":"application/json, text/plain, */*","Content-Type":"application/json","authorization":"Y3JhYjpjcmFi","User-Agent":"axios/0.21.4","Content-Length":35}},"request":{"method":"POST","httpVersion":"1.1","responseUrl":"http://localhost:8090/login","timeout":10000,"headers":{"authorization":"Y3JhYjpjcmFi"},"data":{"username":"foo","password":"bar"}},"data":{"id":1,"username":"foo"}}},{"mime":"text/html","value":"[object Object]"}]},{"kind":1,"language":"markdown","value":"# Добавление бюджета","outputs":[]},{"kind":2,"language":"rest-book","value":"POST http://localhost:8090/budgets\nauthorization: Y3JhYjpjcmFi\nUser-Id: 1\n\n{\n \"name\": \"Личный\"\n}","outputs":[{"mime":"x-application/rest-book","value":{"status":200,"statusText":"OK","headers":{"Date":"Sun, 24 Nov 2024 09:00:36 GMT","Content-Type":"application/json","Content-Length":"60"},"config":{"timeout":10000,"xsrfCookieName":"XSRF-TOKEN","xsrfHeaderName":"X-XSRF-TOKEN","headers":{"Accept":"application/json, text/plain, */*","Content-Type":"application/json","authorization":"Y3JhYjpjcmFi","User-Id":"1","User-Agent":"axios/0.21.4","Content-Length":23}},"request":{"method":"POST","httpVersion":"1.1","responseUrl":"http://localhost:8090/budgets","timeout":10000,"headers":{"authorization":"Y3JhYjpjcmFi","User-Id":"1"},"data":{"name":"Личный"}},"data":{"id":1,"name":"Личный","startDay":0,"monthlyLimit":0}}},{"mime":"text/x-json","value":{"status":200,"statusText":"OK","headers":{"Date":"Sun, 24 Nov 2024 09:00:36 GMT","Content-Type":"application/json","Content-Length":"60"},"config":{"timeout":10000,"xsrfCookieName":"XSRF-TOKEN","xsrfHeaderName":"X-XSRF-TOKEN","headers":{"Accept":"application/json, text/plain, */*","Content-Type":"application/json","authorization":"Y3JhYjpjcmFi","User-Id":"1","User-Agent":"axios/0.21.4","Content-Length":23}},"request":{"method":"POST","httpVersion":"1.1","responseUrl":"http://localhost:8090/budgets","timeout":10000,"headers":{"authorization":"Y3JhYjpjcmFi","User-Id":"1"},"data":{"name":"Личный"}},"data":{"id":1,"name":"Личный","startDay":0,"monthlyLimit":0}}},{"mime":"text/html","value":"[object Object]"}]}] \ No newline at end of file diff --git a/migrations/20241109180651_add_users_table.sql b/migrations/20241109180651_add_users_table.sql index 82091c8..db87a41 100644 --- a/migrations/20241109180651_add_users_table.sql +++ b/migrations/20241109180651_add_users_table.sql @@ -4,7 +4,7 @@ CREATE TABLE IF NOT EXISTS users ( id SERIAL PRIMARY KEY, username TEXT UNIQUE NOT NULL, password TEXT NOT NULL, - created_at TIMESTAMP + created_at TIMESTAMP DEFAULT NOW() ); -- +goose StatementEnd diff --git a/migrations/20241120173848_add_budgets_table.sql b/migrations/20241120173848_add_budgets_table.sql index c0d85d8..a5a83c5 100644 --- a/migrations/20241120173848_add_budgets_table.sql +++ b/migrations/20241120173848_add_budgets_table.sql @@ -5,7 +5,7 @@ CREATE TABLE IF NOT EXISTS budgets ( name TEXT NOT NULL, start_day INT NOT NULL, monthly_limit INT DEFAULT 0, - created_at TIMESTAMP + created_at TIMESTAMP DEFAULT NOW() ); -- +goose StatementEnd diff --git a/migrations/20241120173924_add_categories_table.sql b/migrations/20241120173924_add_categories_table.sql index cc085ee..9ed10ce 100644 --- a/migrations/20241120173924_add_categories_table.sql +++ b/migrations/20241120173924_add_categories_table.sql @@ -7,7 +7,7 @@ CREATE TABLE IF NOT EXISTS categories ( favorite BOOLEAN DEFAULT FALSE, monthly_limit INT DEFAULT 0, UNIQUE (budget_id, name), - created_at TIMESTAMP + created_at TIMESTAMP DEFAULT NOW() ); -- +goose StatementEnd diff --git a/migrations/20241120174237_add_positions_table.sql b/migrations/20241120174237_add_positions_table.sql index 9a7365f..fecc304 100644 --- a/migrations/20241120174237_add_positions_table.sql +++ b/migrations/20241120174237_add_positions_table.sql @@ -7,7 +7,7 @@ CREATE TABLE IF NOT EXISTS wastes ( amount FLOAT NOT NULL, budget_id INT REFERENCES budgets(id) ON DELETE CASCADE, category_id INT REFERENCES categories(id) ON DELETE RESTRICT, - created_at TIMESTAMP + created_at TIMESTAMP DEFAULT NOW() ); -- +goose StatementEnd diff --git a/migrations/20241120175618_add_users_budgets_table.sql b/migrations/20241120175618_add_users_budgets_table.sql index e870020..635b43c 100644 --- a/migrations/20241120175618_add_users_budgets_table.sql +++ b/migrations/20241120175618_add_users_budgets_table.sql @@ -4,7 +4,7 @@ CREATE TABLE IF NOT EXISTS users_budgets ( id SERIAL PRIMARY KEY, user_id INT REFERENCES users(id) ON DELETE CASCADE, budget_id INT REFERENCES budgets(id) ON DELETE RESTRICT, - created_at TIMESTAMP + created_at TIMESTAMP DEFAULT NOW() ); -- +goose StatementEnd