diff --git a/.gitignore b/.gitignore index d88d613..afffe10 100644 --- a/.gitignore +++ b/.gitignore @@ -15,3 +15,4 @@ __debug_bin* static testdata/ /payouts.properties +/payouts.db diff --git a/cmd/payouts/main.go b/cmd/payouts/main.go index c411e87..bde76c5 100644 --- a/cmd/payouts/main.go +++ b/cmd/payouts/main.go @@ -1,19 +1,32 @@ package main import ( + "log/slog" + "os" + "go.uber.org/fx" + "go.uber.org/fx/fxevent" "payouts/internal/api" "payouts/internal/config" "payouts/internal/log" + "payouts/internal/service/cache" + "payouts/internal/service/database" ) func main() { app := fx.New( api.Module, + cache.Module, config.Module, + database.Module, + log.Module, + fx.WithLogger(func() fxevent.Logger { + // log internal fx events just to stdout json because app config isn't read yet + return &fxevent.SlogLogger{Logger: slog.New(slog.NewJSONHandler(os.Stdout, nil))} + }), ) app.Run() } diff --git a/config/payouts.properties b/config/payouts.properties index abd5907..cb9fa7f 100644 --- a/config/payouts.properties +++ b/config/payouts.properties @@ -24,8 +24,23 @@ Log.FileEnabled = false # type: sqlite, postgres Database.Type = # connection string: -# sqlite: flibooks.db +# sqlite: payouts.db # postgres: host=127.0.0.1 user=gorm password=gorm dbname=gorm port=5432 sslmode=disable Database.Connection = # DB log level Database.LogLevel = Info +# Trace all requests +Database.TraceRequests = false + +# Session cache TTL +Cache.TTL = 24h + +# Yookassa related props +# Base API Url +YooKassa.BaseUrl = https://api.yookassa.ru/v3 +# Base API key/secret +YooKassa.ApiBaseKey = +YooKassa.ApiBaseSecret = +# Payments API key/secret +YooKassa.ApiPaymentKey = +YooKassa.ApiPaymentSecret = diff --git a/go.mod b/go.mod index 233acdd..d074069 100644 --- a/go.mod +++ b/go.mod @@ -5,13 +5,17 @@ go 1.24.4 require ( github.com/go-viper/encoding/javaproperties v0.1.0 github.com/go-viper/mapstructure/v2 v2.5.0 + github.com/google/uuid v1.6.0 github.com/gorilla/mux v1.8.1 + github.com/jellydator/ttlcache/v3 v3.4.0 + github.com/jinzhu/copier v0.4.0 github.com/ogier/pflag v0.0.1 github.com/orandin/slog-gorm v1.4.0 github.com/prometheus/client_golang v1.23.2 github.com/samber/slog-multi v1.7.1 github.com/spf13/viper v1.21.0 go.uber.org/fx v1.24.0 + golang.org/x/crypto v0.48.0 gorm.io/driver/postgres v1.6.0 gorm.io/driver/sqlite v1.6.0 gorm.io/gorm v1.31.1 @@ -47,9 +51,8 @@ require ( go.uber.org/zap v1.26.0 // indirect go.yaml.in/yaml/v2 v2.4.2 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect - golang.org/x/crypto v0.31.0 // indirect - golang.org/x/sync v0.16.0 // indirect - golang.org/x/sys v0.35.0 // indirect - golang.org/x/text v0.28.0 // indirect + golang.org/x/sync v0.19.0 // indirect + golang.org/x/sys v0.41.0 // indirect + golang.org/x/text v0.34.0 // indirect google.golang.org/protobuf v1.36.8 // indirect ) diff --git a/go.sum b/go.sum index 72b366b..c98113e 100644 --- a/go.sum +++ b/go.sum @@ -15,6 +15,8 @@ github.com/go-viper/mapstructure/v2 v2.5.0 h1:vM5IJoUAy3d7zRSVtIwQgBj7BiWtMPfmPE github.com/go-viper/mapstructure/v2 v2.5.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= @@ -25,6 +27,10 @@ github.com/jackc/pgx/v5 v5.6.0 h1:SWJzexBzPL5jb0GEsrPMLIsi/3jOo7RHlzTjcAeDrPY= github.com/jackc/pgx/v5 v5.6.0/go.mod h1:DNZ/vlrUnhWCoFGxHAG8U2ljioxukquj7utPDgtQdTw= github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo= github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= +github.com/jellydator/ttlcache/v3 v3.4.0 h1:YS4P125qQS0tNhtL6aeYkheEaB/m8HCqdMMP4mnWdTY= +github.com/jellydator/ttlcache/v3 v3.4.0/go.mod h1:Hw9EgjymziQD3yGsQdf1FqFdpp7YjFMd4Srg5EJlgD4= +github.com/jinzhu/copier v0.4.0 h1:w3ciUoD19shMCRargcpm0cm91ytaBhDvuRpz1ODO/U8= +github.com/jinzhu/copier v0.4.0/go.mod h1:DfbEm0FYsaqBcKcFuvmOZb218JkPGtvSHsKg8S8hyyg= github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= @@ -100,14 +106,14 @@ go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI= go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU= go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= -golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U= -golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= -golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw= -golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= -golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI= -golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= -golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng= -golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU= +golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts= +golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos= +golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= +golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k= +golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk= +golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA= google.golang.org/protobuf v1.36.8 h1:xHScyCOEuuwZEc6UtSOvPbAT4zRh0xcNRYekJwfqyMc= google.golang.org/protobuf v1.36.8/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/internal/api/module.go b/internal/api/module.go index 915ad85..09d981c 100644 --- a/internal/api/module.go +++ b/internal/api/module.go @@ -11,6 +11,8 @@ import ( "github.com/prometheus/client_golang/prometheus/promhttp" "go.uber.org/fx" + "payouts/internal/api/payment" + "payouts/internal/api/user" "payouts/internal/api/version" appConfig "payouts/internal/config" "payouts/internal/service/monitoring" @@ -18,8 +20,11 @@ import ( // Module is a fx module var Module = fx.Options( + user.Module, + payment.Module, version.Module, monitoring.Module, + fx.Invoke(RegisterRoutes), ) @@ -32,7 +37,9 @@ type Params struct { AppConfig *appConfig.App - Version version.Handler + PaymentHandler payment.Handler + UserHandler user.Handler + Version version.Handler Metrics monitoring.Metrics } @@ -54,11 +61,16 @@ func RegisterRoutes(p Params, lc fx.Lifecycle) { } apiRouter := router.PathPrefix(BaseRoute).Subrouter() - apiRouter.HandleFunc("/test", func(http.ResponseWriter, *http.Request) { - slog.Info("Test called", slog.String("sample", "value")) - }) - // data + userRouter := apiRouter.PathPrefix(user.BaseRoute).Subrouter() + userRouter.HandleFunc(user.RegisterRoute, p.UserHandler.UserRegister).Methods(http.MethodPost) + userRouter.HandleFunc(user.LoginRoute, p.UserHandler.UserLogin).Methods(http.MethodPost) + + paymentRouter := apiRouter.PathPrefix(payment.BaseRoute).Subrouter() + paymentRouter.HandleFunc(payment.CreateRoute, p.PaymentHandler.PaymentCreate).Methods(http.MethodPost) + paymentRouter.HandleFunc(payment.CallbackRoute, p.PaymentHandler.PaymentCallback).Methods(http.MethodPost) + + // collect api metrics apiRouter.Use(p.Metrics.GetMiddleware()) router.Handle(p.AppConfig.Metrics.Endpoint, promhttp.Handler()) diff --git a/internal/api/payment/module.go b/internal/api/payment/module.go new file mode 100644 index 0000000..b41fc71 --- /dev/null +++ b/internal/api/payment/module.go @@ -0,0 +1,17 @@ +package payment + +import ( + "net/http" + + "go.uber.org/fx" +) + +var Module = fx.Options( + fx.Provide(NewPaymentHandler), +) + +type Handler interface { + GetSbpBanks(http.ResponseWriter, *http.Request) + PaymentCreate(http.ResponseWriter, *http.Request) + PaymentCallback(http.ResponseWriter, *http.Request) +} diff --git a/internal/api/payment/payment_handler.go b/internal/api/payment/payment_handler.go new file mode 100644 index 0000000..9f5e8a5 --- /dev/null +++ b/internal/api/payment/payment_handler.go @@ -0,0 +1,54 @@ +package payment + +import ( + "net/http" + + "go.uber.org/fx" + + "payouts/internal/config" + "payouts/internal/service/cache" + "payouts/internal/service/database" +) + +const ( + BaseRoute = "/payment" + CreateRoute = "/create" + CallbackRoute = "/callback" + BanksRoute = "/sbp/banks" +) + +type paymentHandler struct { + dbService database.Service + cacheService cache.Service +} + +// Params represents the module input params +type Params struct { + fx.In + + AppConfig *config.App + DbService database.Service + CacheService cache.Service +} + +func NewPaymentHandler(p Params) (Handler, error) { + return &paymentHandler{ + dbService: p.DbService, + cacheService: p.CacheService, + }, nil +} + +// GetSbpBanks implements [Handler]. +func (p *paymentHandler) GetSbpBanks(http.ResponseWriter, *http.Request) { + panic("unimplemented") +} + +// PaymentCreate implements [Handler]. +func (p *paymentHandler) PaymentCreate(http.ResponseWriter, *http.Request) { + panic("unimplemented") +} + +// PaymentCallback implements [Handler]. +func (p *paymentHandler) PaymentCallback(http.ResponseWriter, *http.Request) { + panic("unimplemented") +} diff --git a/internal/api/user/module.go b/internal/api/user/module.go new file mode 100644 index 0000000..2041e4b --- /dev/null +++ b/internal/api/user/module.go @@ -0,0 +1,16 @@ +package user + +import ( + "net/http" + + "go.uber.org/fx" +) + +type Handler interface { + UserRegister(http.ResponseWriter, *http.Request) + UserLogin(http.ResponseWriter, *http.Request) +} + +var Module = fx.Options( + fx.Provide(NewUserHandler), +) diff --git a/internal/api/user/user_handler.go b/internal/api/user/user_handler.go new file mode 100644 index 0000000..ee503b8 --- /dev/null +++ b/internal/api/user/user_handler.go @@ -0,0 +1,144 @@ +package user + +import ( + "encoding/json" + "errors" + "log/slog" + "net/http" + "time" + + "github.com/google/uuid" + "github.com/jinzhu/copier" + "go.uber.org/fx" + "golang.org/x/crypto/bcrypt" + + "payouts/internal/config" + "payouts/internal/models" + "payouts/internal/service/cache" + "payouts/internal/service/database" + "payouts/internal/service/database/orm" +) + +const ( + BaseRoute = "/user" + RegisterRoute = "/register" + LoginRoute = "/login" +) + +type userHandler struct { + ttl time.Duration + dbService database.Service + cacheService cache.Service +} + +// Params represents the module input params +type Params struct { + fx.In + + AppConfig *config.App + DbService database.Service + CacheService cache.Service +} + +func NewUserHandler(p Params) (Handler, error) { + return &userHandler{ + ttl: p.AppConfig.Cache.TTL, + dbService: p.DbService, + cacheService: p.CacheService, + }, nil +} + +// UserRegister implements [Handler]. +func (u *userHandler) UserRegister(w http.ResponseWriter, r *http.Request) { + defer r.Body.Close() + + errResponse := func(message string, err error, status int) { + http.Error(w, errors.Join(errors.New(message), err).Error(), status) + } + + user := models.UserRegister{} + err := json.NewDecoder(r.Body).Decode(&user) + if err != nil { + slog.Error("Failed to get password hash", slog.String("error", err.Error())) + errResponse("failed to decode request body", err, http.StatusBadRequest) + return + } + + if user.Passwd != user.PasswdCfm || len(user.Passwd) == 0 || len(user.Phone) == 0 || len(user.TIN) == 0 { + slog.Error("No required parameters passed") + errResponse("invalid parameters", nil, http.StatusBadRequest) + return + } + + hashedPassword, err := bcrypt.GenerateFromPassword([]byte(user.Passwd), bcrypt.DefaultCost) + if err != nil { + slog.Error("Failed to get password hash", slog.String("error", err.Error())) + errResponse("internal error", nil, http.StatusInternalServerError) + return + } + user.PasswdHash = string(hashedPassword) + + ormUser := orm.User{} + copier.Copy(&ormUser, user) + + // todo: add data validation + err = u.dbService.CreateUser(ormUser, database.WithContext(r.Context())) + if err != nil { + slog.Error("Failed to create user", slog.String("error", err.Error())) + errResponse("failed to create user", err, http.StatusBadRequest) + return + } + + w.WriteHeader(http.StatusCreated) +} + +// UserLogin implements [Handler]. +func (u *userHandler) UserLogin(w http.ResponseWriter, r *http.Request) { + defer r.Body.Close() + + errResponse := func(message string, err error, status int) { + w.Header().Set("Content-Type", "text/plain") + http.Error(w, errors.Join(errors.New(message), err).Error(), status) + } + + user := models.UserLoginReq{} + err := json.NewDecoder(r.Body).Decode(&user) + if err != nil { + errResponse("failed to decode request body", err, http.StatusBadRequest) + return + } + + if len(user.Phone) == 0 || len(user.Passwd) == 0 { + slog.Error("No required parameters passed") + errResponse("invalid parameters", nil, http.StatusBadRequest) + return + } + + ormUser, err := u.dbService.GetUser(orm.User{Phone: user.Phone}, database.WithContext(r.Context())) + if err != nil { + errResponse("invalid credentials", nil, http.StatusUnauthorized) + return + } + + err = bcrypt.CompareHashAndPassword([]byte(ormUser.PasswdHash), []byte(user.Passwd)) + if err != nil { + errResponse("invalid credentials", nil, http.StatusUnauthorized) + return + } + + sessionId := uuid.New().String() + + u.cacheService.PutSession(sessionId, ormUser) + + resp := models.UserLoginResp{ + Token: sessionId, + TokenTtl: time.Now().Add(u.ttl).Unix(), + } + + w.Header().Set("Content-Type", "application/json") + err = json.NewEncoder(w).Encode(resp) + if err != nil { + errResponse("failed to encode response", err, http.StatusInternalServerError) + return + } +} diff --git a/internal/config/app.go b/internal/config/app.go index 6e1010a..3be3040 100644 --- a/internal/config/app.go +++ b/internal/config/app.go @@ -2,13 +2,18 @@ package config import ( logging "payouts/internal/log/config" + cache "payouts/internal/service/cache/config" database "payouts/internal/service/database/config" monitoring "payouts/internal/service/monitoring/config" + yookassa "payouts/internal/service/yookassa/config" ) type App struct { Server Server Metrics monitoring.Metrics Database database.Database + Cache cache.Cache Log logging.Log + + YooKassa yookassa.YooKassa } diff --git a/internal/config/module.go b/internal/config/module.go index ac6e2b4..0c836ba 100644 --- a/internal/config/module.go +++ b/internal/config/module.go @@ -2,6 +2,7 @@ package config import ( "fmt" + "os" "path/filepath" "strings" @@ -17,6 +18,7 @@ import ( const ( ConfigPathArg = "config-path" ConfigPathDefault = "./payouts.properties" + envConfigFile = "CONFIG_PATH" ) var Module = fx.Provide(NewAppConfig) @@ -36,7 +38,7 @@ func getConfigData(filePath string) (string, string, string) { func NewAppConfig() (*App, error) { mainConfig := &App{} - configPaths := []string{ConfigPathDefault} + configPaths := []string{ConfigPathDefault, os.Getenv(envConfigFile)} configPath := pflag.String(ConfigPathArg, "", "") pflag.Parse() diff --git a/internal/models/user.go b/internal/models/user.go new file mode 100644 index 0000000..b170025 --- /dev/null +++ b/internal/models/user.go @@ -0,0 +1,20 @@ +package models + +type UserRegister struct { + TIN string `json:"tin"` + Phone string `json:"phone"` + Passwd string `json:"password,omitempty"` + PasswdCfm string `json:"password_cfm,omitempty"` + PasswdHash string `json:"-"` +} + +type UserLoginReq struct { + Phone string `json:"phone"` + Passwd string `json:"password"` + PasswdHash string `json:"-"` +} + +type UserLoginResp struct { + Token string `json:"token"` + TokenTtl int64 `json:"token_ttl"` +} diff --git a/internal/service/cache/cache_service.go b/internal/service/cache/cache_service.go new file mode 100644 index 0000000..e2186d5 --- /dev/null +++ b/internal/service/cache/cache_service.go @@ -0,0 +1,39 @@ +package cache + +import ( + "time" + + "github.com/jellydator/ttlcache/v3" + + "payouts/internal/service/database/orm" +) + +type cacheService struct { + cache *ttlcache.Cache[string, orm.User] +} + +func NewSessionCache(ttl time.Duration) (Service, error) { + return &cacheService{ + cache: ttlcache.New(ttlcache.WithTTL[string, orm.User](ttl)), + }, nil +} + +// PutSession implements [Service]. +func (c *cacheService) PutSession(sessionID string, user orm.User) { + c.cache.Set(sessionID, user, ttlcache.DefaultTTL) +} + +// GetUserFromSession implements [Service]. +func (c *cacheService) GetSession(sessionId string) (orm.User, error) { + if !c.cache.Has(sessionId) { + return orm.User{}, NoSessionFound + } + cachedItem := c.cache.Get(sessionId) + + return cachedItem.Value(), nil +} + +// StartBackground implements [Service]. +func (c *cacheService) StartBackground() { + c.cache.Start() +} diff --git a/internal/service/cache/config/cache.go b/internal/service/cache/config/cache.go new file mode 100644 index 0000000..f47b3d0 --- /dev/null +++ b/internal/service/cache/config/cache.go @@ -0,0 +1,7 @@ +package config + +import "time" + +type Cache struct { + TTL time.Duration +} diff --git a/internal/service/cache/module.go b/internal/service/cache/module.go new file mode 100644 index 0000000..ba4b7f4 --- /dev/null +++ b/internal/service/cache/module.go @@ -0,0 +1,50 @@ +package cache + +import ( + "context" + "errors" + + "go.uber.org/fx" + + "payouts/internal/config" + "payouts/internal/service/database/orm" +) + +var Module = fx.Options( + fx.Provide(New), + + fx.Invoke(StartCache), +) + +var NoSessionFound = errors.New("no session found") + +type Service interface { + PutSession(string, orm.User) + GetSession(string) (orm.User, error) + StartBackground() +} + +type Params struct { + fx.In + + AppConfig *config.App +} + +func New(p Params) (Service, error) { + return NewSessionCache(p.AppConfig.Cache.TTL) +} + +// RegisterRoutes registers the api routes and starts the http server +func StartCache(s Service, lc fx.Lifecycle) { + lc.Append(fx.Hook{ + OnStart: func(c context.Context) error { + go func() { + s.StartBackground() + }() + return nil + }, + OnStop: func(ctx context.Context) error { + return nil + }, + }) +} diff --git a/internal/service/database/config/database.go b/internal/service/database/config/database.go index 8c7c3ac..bae4f35 100644 --- a/internal/service/database/config/database.go +++ b/internal/service/database/config/database.go @@ -1,7 +1,8 @@ package config type Database struct { - Type string - Connection string - LogLevel string + Type string + Connection string + LogLevel string + TraceRequests bool } diff --git a/internal/service/database/db_service.go b/internal/service/database/db_service.go index 4ce6221..9b52d82 100644 --- a/internal/service/database/db_service.go +++ b/internal/service/database/db_service.go @@ -1,12 +1,17 @@ package database import ( + "context" "errors" + "log/slog" slogGorm "github.com/orandin/slog-gorm" "gorm.io/driver/postgres" "gorm.io/driver/sqlite" "gorm.io/gorm" + + "payouts/internal/service/database/config" + "payouts/internal/service/database/orm" ) type dbService struct { @@ -14,31 +19,67 @@ type dbService struct { db *gorm.DB } -func NewDatabaseService(dbType, connection, logLevel string) (DatabaseService, error) { +func NewDatabaseService(conf config.Database) (Service, error) { var dialector gorm.Dialector - switch dbType { + switch conf.Type { case "sqlite": - dialector = sqlite.Open(connection) + dialector = sqlite.Open(conf.Connection) case "postgres": - dialector = postgres.Open(connection) + dialector = postgres.Open(conf.Connection) default: return nil, errors.New("unknown dbType") } + level := slog.LevelInfo + level.UnmarshalText([]byte(conf.LogLevel)) + + slogGormOpts := []slogGorm.Option{ + slogGorm.SetLogLevel(slogGorm.DefaultLogType, level), + } + if conf.TraceRequests { + slogGormOpts = append(slogGormOpts, slogGorm.WithTraceAll()) + } + + slogGorm := slogGorm.New(slogGormOpts...) + db, err := gorm.Open(dialector, &gorm.Config{ - Logger: slogGorm.New(), + Logger: slogGorm, }) if err == nil { db.DB() - db.AutoMigrate() - // db.LogMode(true) + db.AutoMigrate(&orm.User{}) } result := &dbService{} - result.dbType = dbType + result.dbType = conf.Type result.db = db return result, err } + +func getParams(options ...Optional) *params { + params := ¶ms{ + ctx: context.Background(), + } + for _, opt := range options { + opt(params) + } + return params +} + +// AddUser implements [Service]. +func (d *dbService) CreateUser(userModel orm.User, opts ...Optional) error { + p := getParams(opts...) + + return gorm.G[orm.User](d.db).Create(p.ctx, &userModel) +} + +// GetUser implements [Service]. +func (d *dbService) GetUser(userModel orm.User, opts ...Optional) (orm.User, error) { + p := getParams(opts...) + + userResp, err := gorm.G[orm.User](d.db).Where(&userModel).First(p.ctx) + return userResp, err +} diff --git a/internal/service/database/module.go b/internal/service/database/module.go index 4c259ba..89cc5e4 100644 --- a/internal/service/database/module.go +++ b/internal/service/database/module.go @@ -1,16 +1,32 @@ package database import ( - "payouts/internal/config" + "context" "go.uber.org/fx" + + "payouts/internal/config" + "payouts/internal/service/database/orm" ) var Module = fx.Options( fx.Provide(New), ) -type DatabaseService interface { +type params struct { + ctx context.Context +} +type Optional func(*params) + +func WithContext(ctx context.Context) Optional { + return func(p *params) { + p.ctx = ctx + } +} + +type Service interface { + CreateUser(user orm.User, opts ...Optional) error + GetUser(user orm.User, opts ...Optional) (orm.User, error) } // Params represents the module input params @@ -21,6 +37,6 @@ type Params struct { } // NewPersistence instantiates the persistence module -func New(p Params) (DatabaseService, error) { - return NewDatabaseService(p.AppConfig.Database.Type, p.AppConfig.Database.Connection, p.AppConfig.Database.LogLevel) +func New(p Params) (Service, error) { + return NewDatabaseService(p.AppConfig.Database) } diff --git a/internal/service/database/orm/user.go b/internal/service/database/orm/user.go new file mode 100644 index 0000000..34a6c42 --- /dev/null +++ b/internal/service/database/orm/user.go @@ -0,0 +1,10 @@ +package orm + +import "gorm.io/gorm" + +type User struct { + gorm.Model + TIN string + Phone string `gorm:"uniqueIndex:idx_phone"` + PasswdHash string +} diff --git a/internal/service/yookassa/config/yookassa.go b/internal/service/yookassa/config/yookassa.go new file mode 100644 index 0000000..80011e7 --- /dev/null +++ b/internal/service/yookassa/config/yookassa.go @@ -0,0 +1,10 @@ +package config + +type YooKassa struct { + BaseUrl string + + ApiBaseKey string + ApiBaseSecret string + ApiPaymentKey string + ApiPaymentSecret string +} diff --git a/internal/service/yookassa/module.go b/internal/service/yookassa/module.go new file mode 100644 index 0000000..c1433bb --- /dev/null +++ b/internal/service/yookassa/module.go @@ -0,0 +1,37 @@ +package yookassa + +import ( + "context" + + "go.uber.org/fx" + + "payouts/internal/config" +) + +var Module = fx.Options( + fx.Provide(New), +) + +type params struct { + ctx context.Context +} +type Optional func(*params) + +func WithContext(ctx context.Context) Optional { + return func(p *params) { + p.ctx = ctx + } +} + +type Service interface { +} + +type Param struct { + fx.In + + AppConfig *config.App +} + +func New(p Param) (Service, error) { + return NewYookassaService(p.AppConfig.YooKassa) +} diff --git a/internal/service/yookassa/yookassa_service.go b/internal/service/yookassa/yookassa_service.go new file mode 100644 index 0000000..8635b9e --- /dev/null +++ b/internal/service/yookassa/yookassa_service.go @@ -0,0 +1,13 @@ +package yookassa + +import "payouts/internal/service/yookassa/config" + +type yookassaService struct { + conf config.YooKassa +} + +func NewYookassaService(conf config.YooKassa) (Service, error) { + return &yookassaService{ + conf: conf, + }, nil +}