diff --git a/backend/device/controller/controller.go b/backend/device/controller/controller.go index 86dfa09..74488a3 100644 --- a/backend/device/controller/controller.go +++ b/backend/device/controller/controller.go @@ -32,7 +32,7 @@ func (c *Controller) AddDeviceHandler(writer http.ResponseWriter, request *http. SessionId: request.SessionId, RefreshToken: request.RefreshToken, } - err = c.service.Add(ctx, d) + err = c.service.Add(ctx, &d) if err != nil { onError(writer, err) return diff --git a/backend/device/repository/repository.go b/backend/device/repository/repository.go index 94b8a35..0d08c24 100644 --- a/backend/device/repository/repository.go +++ b/backend/device/repository/repository.go @@ -3,6 +3,7 @@ package repository import ( "context" "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" "go.mongodb.org/mongo-driver/mongo" "receipt_collector/dispose" "receipt_collector/nalogru/device" @@ -12,18 +13,19 @@ type Repository struct { m *mongo.Client } -//NewRepository creates Repository. +// NewRepository creates Repository. func NewRepository(m *mongo.Client) *Repository { return &Repository{m: m} } -func (r *Repository) Add(ctx context.Context, d device.Device) error { +func (r *Repository) Add(ctx context.Context, d *device.Device) error { collection := r.getCollection() - _, err := collection.InsertOne(ctx, d) + document, err := collection.InsertOne(ctx, d) + d.Id = document.InsertedID.(primitive.ObjectID) return err } -//All returns all devices. +// All returns all devices. func (r *Repository) All(ctx context.Context) ([]device.Device, error) { collection := r.getCollection() cursor, err := collection.Find(ctx, bson.D{}) diff --git a/backend/device/service.go b/backend/device/service.go index 6346840..61f16db 100644 --- a/backend/device/service.go +++ b/backend/device/service.go @@ -7,13 +7,13 @@ import ( "receipt_collector/nalogru/device" ) -//Service to manage devices. +// Service to manage devices. type Service struct { r *repository.Repository devices []ForRent } -//NewService creates instance of Service. +// NewService creates instance of Service. func NewService(ctx context.Context, r *repository.Repository) (*Service, error) { all, err := r.All(ctx) if err != nil { @@ -32,42 +32,74 @@ func NewService(ctx context.Context, r *repository.Repository) (*Service, error) return s, nil } -//Add adds new device. -func (s *Service) Add(ctx context.Context, d device.Device) error { +// Add adds new device. +func (s *Service) Add(ctx context.Context, d *device.Device) error { for _, v := range s.devices { if v.ClientSecret == d.ClientSecret { return errors.New("that device already added") } } + d.Update = s.updateDeviceFunc(ctx, *d) + + forRent := ForRent{ + Device: *d, + IsRent: false, + } + s.devices = append(s.devices, forRent) return s.r.Add(ctx, d) } -//Count returns devices count. +// Count returns devices count. func (s *Service) Count(ctx context.Context) (int, error) { + if ctx.Err() != nil { + return -1, ctx.Err() + } return len(s.devices), nil } -//Rent device. +// Rent any device. func (s *Service) Rent(ctx context.Context) (*device.Device, error) { + if ctx.Err() != nil { + return nil, ctx.Err() + } for _, v := range s.devices { if v.IsRent == false { - v.IsRent = true + s.rent(&v) return &v.Device, nil } } return nil, errors.New("no available devices found") } -func (s *Service) Update(ctx context.Context, device *device.Device) error { +// RentDevice rent concrete device. +func (s *Service) RentDevice(ctx context.Context, d *device.Device) error { + if ctx.Err() != nil { + return ctx.Err() + } for _, v := range s.devices { - if device.Id == v.Id { - v.Device = *device + if v.Id == d.Id { + if v.IsRent { + return errors.New("device is already used") + } else { + s.rent(&v) + } } } + return errors.New("device not found") +} + +func (s *Service) rent(v *ForRent) { + v.IsRent = true +} + +func (s *Service) Update(ctx context.Context, device *device.Device, sessionId string, refreshToken string) error { + device.SessionId = sessionId + device.RefreshToken = refreshToken return s.r.Update(ctx, device) } -func (s *Service) Free(ctx context.Context, device *device.Device) error { +// Free release the rented device +func (s *Service) Free(_ context.Context, device *device.Device) error { for _, v := range s.devices { if device.Id == v.Id { v.IsRent = false @@ -76,3 +108,32 @@ func (s *Service) Free(ctx context.Context, device *device.Device) error { } return errors.New("device not found") } + +// All return all registered devices +func (s *Service) All(_ context.Context) []*device.Device { + res := make([]*device.Device, len(s.devices)) + for i, d := range s.devices { + res[i] = &d.Device + } + return res +} + +func (s *Service) GetByUserId(ctx context.Context, userId string) (*device.Device, error) { + devices, err := s.r.All(ctx) + if err != nil { + return nil, err + } + for _, d := range devices { + if d.UserId == userId { + d.Update = s.updateDeviceFunc(ctx, d) + return &d, nil + } + } + return nil, nil +} + +func (s *Service) updateDeviceFunc(ctx context.Context, d device.Device) func(sessionId string, refreshToken string) error { + return func(sessionId string, refreshToken string) error { + return s.Update(ctx, &d, sessionId, refreshToken) + } +} diff --git a/backend/go.mod b/backend/go.mod index 1edbdf1..ba1cbb0 100644 --- a/backend/go.mod +++ b/backend/go.mod @@ -3,7 +3,8 @@ module receipt_collector go 1.20 require ( - github.com/drypa/ReceiptCollector/api/inside v0.0.0-20230405091352-65e1efb55137 + github.com/drypa/ReceiptCollector/api/inside v0.0.0-20230819122429-dfb6fd2c5a01 + github.com/go-co-op/gocron v1.28.2 github.com/goji/httpauth v0.0.0-20160601135302-2da839ab0f4d github.com/google/uuid v1.3.0 github.com/gorilla/mux v1.8.0 @@ -24,6 +25,7 @@ require ( github.com/xdg-go/scram v1.1.1 // indirect github.com/xdg-go/stringprep v1.0.3 // indirect github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d // indirect + go.uber.org/atomic v1.9.0 // indirect golang.org/x/net v0.8.0 // indirect golang.org/x/sync v0.0.0-20210220032951-036812b2e83c // indirect golang.org/x/sys v0.6.0 // indirect diff --git a/backend/go.sum b/backend/go.sum index 43f55f8..4d1980d 100644 --- a/backend/go.sum +++ b/backend/go.sum @@ -1,8 +1,11 @@ +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/drypa/ReceiptCollector/api/inside v0.0.0-20230405091352-65e1efb55137 h1:2gSdtAgn/1SzR3jIxQybz+G8hFlLLPgwBpwe5se5kf4= -github.com/drypa/ReceiptCollector/api/inside v0.0.0-20230405091352-65e1efb55137/go.mod h1:4ULoAoegdVc7I/NgQ6izbBbXhqheKj8JbANBQ3xfVN0= +github.com/drypa/ReceiptCollector/api/inside v0.0.0-20230819122429-dfb6fd2c5a01 h1:HCdDtLl5QNclGP45Q6dt9d5XEfA5OMf+WRdLUOqiJx4= +github.com/drypa/ReceiptCollector/api/inside v0.0.0-20230819122429-dfb6fd2c5a01/go.mod h1:4ULoAoegdVc7I/NgQ6izbBbXhqheKj8JbANBQ3xfVN0= +github.com/go-co-op/gocron v1.28.2 h1:H9oHUGH+9HZ5mAorbnzRjzXLf4poP+ctZdbtaKRYagc= +github.com/go-co-op/gocron v1.28.2/go.mod h1:39f6KNSGVOU1LO/ZOoZfcSxwlsJDQOKSu8erN0SH48Y= github.com/goji/httpauth v0.0.0-20160601135302-2da839ab0f4d h1:lBXNCxVENCipq4D1Is42JVOP4eQjlB8TQ6H69Yx5J9Q= github.com/goji/httpauth v0.0.0-20160601135302-2da839ab0f4d/go.mod h1:nnjvkQ9ptGaCkuDUx6wNykzzlUixGxvkme+H/lnzb+A= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= @@ -20,22 +23,35 @@ github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI= github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= github.com/klauspost/compress v1.13.6 h1:P76CopJELS0TiO2mebmnzgWaajssP/EszplttgQxcgc= github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk= -github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= +github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= -github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe h1:iruDEfMl2E6fbMZ9s0scYfZQ84/6SPL6zC8ACM2oIL0= github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc= +github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs= github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro= +github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= +github.com/rogpeppe/go-internal v1.8.1 h1:geMPLpDpQOgVyCg5z5GoRwLHepNdb71NXb67XFkP+Eg= +github.com/rogpeppe/go-internal v1.8.1/go.mod h1:JeRgkft04UBgHMgCIwADu4Pn6Mtm5d4nPKWu0nJ5d+o= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8= +github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk= github.com/tidwall/pretty v1.0.2 h1:Z7S3cePv9Jwm1KwS0513MRaoUe3S01WPbLNV40pwWZU= github.com/tidwall/pretty v1.0.2/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk= @@ -49,6 +65,8 @@ github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d h1:splanxYIlg+5LfHAM github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d/go.mod h1:rHwXgn7JulP+udvsHwJoVG1YGAP6VLg4y9I5dyZdqmA= go.mongodb.org/mongo-driver v1.11.3 h1:Ql6K6qYHEzB6xvu4+AU0BoRoqf9vFPcc4o7MUIdPW8Y= go.mongodb.org/mongo-driver v1.11.3/go.mod h1:PTSz5yu21bkT/wXpkS7WR5f0ddqw5quethTUn9WM+2g= +go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE= +go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.7.0 h1:AvwMYaRytfdeVt3u6mLaxYtErKYjxA2OXjJ1HHq6t3A= golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU= @@ -79,6 +97,8 @@ google.golang.org/protobuf v1.28.1 h1:d0NfwRgPtno5B1Wa6L2DAG+KivqkdutMf1UhdNx175 google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/backend/internal/processor.go b/backend/internal/processor.go index 12a7858..658366f 100644 --- a/backend/internal/processor.go +++ b/backend/internal/processor.go @@ -11,6 +11,8 @@ type AccountProcessor interface { GetLoginLink(ctx context.Context, in *api.GetLoginLinkRequest) (*api.LoginLinkResponse, error) GetUsers(ctx context.Context, req *api.NoParams) (*api.GetUsersResponse, error) GetUser(ctx context.Context, in *api.GetUserRequest, opts ...grpc.CallOption) (*api.GetUserResponse, error) + RegisterUser(ctx context.Context, in *api.UserRegistrationRequest, opts ...grpc.CallOption) (*api.UserRegistrationResponse, error) + VerifyPhone(ctx context.Context, req *api.PhoneVerificationRequest) (*api.ErrorResponse, error) } // ReceiptProcessor is an interface for process receipt requests. diff --git a/backend/internal/server.go b/backend/internal/server.go index 540643c..3ceeee3 100644 --- a/backend/internal/server.go +++ b/backend/internal/server.go @@ -50,3 +50,15 @@ func (s *server) GetRawReceipt(ctx context.Context, in *api.GetRawReceiptReportR processor := *(s.receiptProcessor) return processor.GetRawReceipt(ctx, in) } + +// RegisterUser add new user and send registration by phone request to nalog api. +func (s *server) RegisterUser(ctx context.Context, req *api.UserRegistrationRequest) (*api.UserRegistrationResponse, error) { + processor := *(s.accountProcessor) + return processor.RegisterUser(ctx, req) +} + +// VerifyPhone validate phone number through SMS. +func (s *server) VerifyPhone(ctx context.Context, req *api.PhoneVerificationRequest) (*api.ErrorResponse, error) { + processor := *(s.accountProcessor) + return processor.VerifyPhone(ctx, req) +} diff --git a/backend/main.go b/backend/main.go index 5e268ba..5b67e46 100644 --- a/backend/main.go +++ b/backend/main.go @@ -36,6 +36,7 @@ var mongoUser = os.Getenv("MONGO_LOGIN") var mongoSecret = os.Getenv("MONGO_SECRET") var openUrl = os.Getenv("OPEN_URL") var templatePath = os.Getenv("TEMPLATES_PATH") +var clientSecret = os.Getenv("CLIENT_SECRET") func main() { log.SetOutput(os.Stdout) @@ -56,14 +57,7 @@ func main() { log.Printf("Failed to create device service: %v\n", err) return } - - d, err := deviceService.Rent(ctx) - if err != nil { - log.Println("Failed to rent device") - //return - } - - nalogruClient := nalogru.NewClient(baseAddress, d) + nalogruClient := nalogru.NewClient(baseAddress) receiptRepository := receipts.NewRepository(client) userRepository := users.NewRepository(client) marketRepository := markets.NewRepository(client) @@ -80,15 +74,16 @@ func main() { // } //}() - go worker.GetReceiptStart(ctx, settings) + //go worker.GetReceiptStart(ctx, settings) //go worker.UpdateRawReceiptStart(ctx, settings) + worker.GetElectronicReceiptStart(ctx) generator := login_url.New(openUrl) creds, err := credentials.NewServerTLSFromFile("/usr/share/receipts/ssl/certs/certificate.crt", "/usr/share/receipts/ssl/certs/private.key") if err != nil { log.Fatalf("failed to load TLS keys: %v", err) } - var accountProcessor internal.AccountProcessor = login_url.NewProcessor(&userRepository, generator) + var accountProcessor internal.AccountProcessor = users.NewProcessor(&userRepository, generator, nalogruClient, deviceService, clientSecret) r := render.New(templatePath) var receiptProcessor internal.ReceiptProcessor = receipts.NewProcessor(&receiptRepository, r) @@ -96,7 +91,7 @@ func main() { go internal.Serve(":15000", creds, &accountProcessor, &receiptProcessor) go reports.Serve(":15001", creds, &userRepository, &receiptReportRepository) - server := startServer(nalogruClient, receiptRepository, userRepository, marketRepository, wasteRepository, deviceService) + server := startServer(receiptRepository, userRepository, marketRepository, wasteRepository, deviceService) sigChan := make(chan os.Signal) signal.Notify(sigChan, os.Kill) @@ -120,8 +115,7 @@ func getMongoClient() (*mongo.Client, error) { return mongo_client.New(settings) } -func startServer(nalogruClient *nalogru.Client, - receiptRepository receipts.Repository, +func startServer(receiptRepository receipts.Repository, userRepository users.Repository, marketRepository markets.Repository, wasteRepository waste.Repository, @@ -129,7 +123,7 @@ func startServer(nalogruClient *nalogru.Client, marketsController := markets.New(marketRepository) deviceController := controller.NewController(devices) - receiptsController := receipts.New(receiptRepository, nalogruClient) + receiptsController := receipts.New(receiptRepository) usersController := users.New(userRepository) wasteController := waste.New(wasteRepository) basicAuth := auth.New(userRepository) diff --git a/backend/nalogru/client.go b/backend/nalogru/client.go index 464e492..0c8128a 100644 --- a/backend/nalogru/client.go +++ b/backend/nalogru/client.go @@ -17,17 +17,14 @@ import ( type Client struct { BaseAddress string - device *device.Device } -var AuthError = errors.New("auth failed") var InternalError = errors.New("internal failed") // NewClient - creates instance of Client. -func NewClient(baseAddress string, device *device.Device) *Client { +func NewClient(baseAddress string) *Client { return &Client{ BaseAddress: baseAddress, - device: device, } } @@ -36,7 +33,7 @@ const ( ) // CheckReceiptExist send request to check receipt exist in Nalog.ru api. -func (nalogruClient Client) CheckReceiptExist(queryString string) (bool, error) { +func (nalogruClient *Client) CheckReceiptExist(queryString string, device *device.Device) (bool, error) { client := createHttpClient() url, err := buildCheckReceiptUrl(nalogruClient.BaseAddress, queryString) if err != nil { @@ -50,14 +47,14 @@ func (nalogruClient Client) CheckReceiptExist(queryString string) (bool, error) log.Printf("Could't not create request for %s", url) return false, err } - addHeaders(request, nalogruClient.device.Id.Hex()) + addHeaders(request, device.Id.Hex()) resp, err := client.Do(request) if err != nil { log.Printf("Could't check receipt %s", url) return false, err } - defer resp.Body.Close() + defer dispose.Dispose(resp.Body.Close, "Can't close HTTP response body") if resp.StatusCode == http.StatusOK || resp.StatusCode == http.StatusNoContent { log.Println("Check passed") return true, nil @@ -67,7 +64,7 @@ func (nalogruClient Client) CheckReceiptExist(queryString string) (bool, error) } func createHttpClient() *http.Client { - return &http.Client{Timeout: time.Second * 10} + return &http.Client{Timeout: time.Minute} } // TicketIdRequest is request object to get Ticket id. @@ -83,9 +80,14 @@ type TicketIdResponse struct { Status int `json:"status"` } +type PhoneAuthRequest struct { + ClientSecret string `json:"client_secret"` + Os string `json:"os"` + Phone string `json:"phone"` +} + // GetTicketId - send ticket id request to nalog.ru API. -func (nalogruClient *Client) GetTicketId(queryString string) (string, error) { - client := createHttpClient() +func (nalogruClient *Client) GetTicketId(queryString string, device *device.Device) (string, error) { payload := TicketIdRequest{Qr: queryString} req, err := json.Marshal(payload) @@ -95,15 +97,13 @@ func (nalogruClient *Client) GetTicketId(queryString string) (string, error) { reader := bytes.NewReader(req) url := nalogruClient.BaseAddress + "/v2/ticket" request, err := http.NewRequest(http.MethodPost, url, reader) - addHeaders(request, nalogruClient.device.Id.Hex()) - addAuth(request, nalogruClient.device.SessionId) - res, err := sendRequest(request, client) + res, err := nalogruClient.sendAuthenticatedRequest(request, device) if err != nil { log.Printf("Can't POST %s\n", url) return "", err } - defer res.Body.Close() + defer dispose.Dispose(res.Body.Close, "Can't close HTTP response body") body, err := readBody(res) if err != nil { @@ -129,11 +129,7 @@ func (nalogruClient *Client) GetTicketId(queryString string) (string, error) { return "", err } - if res.StatusCode == http.StatusUnauthorized { - err = AuthError - } else { - err = InternalError - } + err = InternalError return "", err } @@ -147,15 +143,6 @@ func (nalogruClient *Client) GetTicketId(queryString string) (string, error) { return ticketIdResp.Id, nil } -func (nalogruClient *Client) GetDevice() device.Device { - return device.Device{ - ClientSecret: nalogruClient.device.ClientSecret, - SessionId: nalogruClient.device.SessionId, - RefreshToken: nalogruClient.device.RefreshToken, - Id: nalogruClient.device.Id, - } -} - func readBody(res *http.Response) ([]byte, error) { defer dispose.Dispose(res.Body.Close, "failed to close response body.") var bodyReader io.ReadCloser @@ -176,20 +163,17 @@ func readBody(res *http.Response) ([]byte, error) { } // GetTicketById get ticket by id from nalog.ru api. -func (nalogruClient *Client) GetTicketById(id string) (*TicketDetails, error) { - client := createHttpClient() +func (nalogruClient *Client) GetTicketById(id string, device *device.Device) (*TicketDetails, error) { url := nalogruClient.BaseAddress + "/v2/tickets/" + id request, err := http.NewRequest(http.MethodGet, url, nil) - addHeaders(request, nalogruClient.device.Id.Hex()) - addAuth(request, nalogruClient.device.SessionId) - res, err := sendRequest(request, client) + res, err := nalogruClient.sendAuthenticatedRequest(request, device) if err != nil { log.Printf("Can't GET %s\n", url) return nil, err } - defer res.Body.Close() + defer dispose.Dispose(res.Body.Close, "Can't close HTTP response body") all, err := readBody(res) if err != nil { @@ -197,10 +181,6 @@ func (nalogruClient *Client) GetTicketById(id string) (*TicketDetails, error) { return nil, err } - if res.StatusCode == http.StatusUnauthorized { - return nil, AuthError - } - if res.StatusCode != http.StatusOK { log.Printf("GET receipt error: %d\n", res.StatusCode) err = os.WriteFile("/var/lib/receipts/error/"+id+".json", all, 0644) @@ -221,6 +201,90 @@ func (nalogruClient *Client) GetTicketById(id string) (*TicketDetails, error) { return details, nil } +// GetElectronicTickets request all electronic receipts added by email or phone from nalog.ru api. +func (nalogruClient *Client) GetElectronicTickets(device *device.Device) ([]*TicketDetails, error) { + + url := nalogruClient.BaseAddress + "/v2/tickets-with-electro" + request, err := http.NewRequest(http.MethodGet, url, nil) + + res, err := nalogruClient.sendAuthenticatedRequest(request, device) + + if err != nil { + log.Printf("Can't GET %s\n", url) + return nil, err + } + defer dispose.Dispose(res.Body.Close, "Can't close response body") + + if res.StatusCode != http.StatusOK { + log.Printf("GET electronic receipts error: %d\n", res.StatusCode) + return nil, err + } + all, err := readBody(res) + if err != nil { + log.Printf("failed to read response body. status code %d error: %v\n", res.StatusCode, err) + return nil, err + } + err = os.WriteFile("/var/lib/receipts/electro/1.json", all, 0644) + var tickets []*TicketDetails + + err = json.Unmarshal(all, &tickets) + if err != nil { + log.Println("Can't decode response body") + + return nil, err + } + + return tickets, nil +} + +func (nalogruClient *Client) AuthByPhone(device *device.Device) error { + payload := PhoneAuthRequest{ + ClientSecret: device.ClientSecret, + Os: "android", + Phone: device.Phone, + } + + req, err := json.Marshal(payload) + if err != nil { + log.Println("Unable to serialize request") + return err + } + reader := bytes.NewReader(req) + url := nalogruClient.BaseAddress + "/v2/auth/phone/request" + request, err := http.NewRequest(http.MethodPost, url, reader) + addHeaders(request, device.Id.Hex()) + client := createHttpClient() + _, err = sendRequest(request, client) + + if err != nil { + log.Printf("Can't POST %s\n", url) + return err + } + + return nil +} + +func (nalogruClient *Client) sendAuthenticatedRequest(r *http.Request, device *device.Device) (*http.Response, error) { + addHeaders(r, device.Id.Hex()) + addAuth(r, device.SessionId) + client := createHttpClient() + res, err := sendRequest(r, client) + + if err != nil { + return nil, err + } + + if res.StatusCode == http.StatusUnauthorized { + err = nalogruClient.RefreshSession(device) + if err != nil { + log.Printf("failed to refresh session. %v\n", err) + return nil, err + } + res, err = sendRequest(r, client) + } + return res, err +} + type RefreshRequest struct { ClientSecret string `json:"client_secret"` RefreshToken string `json:"refresh_token"` @@ -231,12 +295,26 @@ type RefreshResponse struct { RefreshToken string `json:"refresh_token"` } -func (nalogruClient *Client) RefreshSession() error { +type PhoneVerificationRequest struct { + ClientSecret string `json:"client_secret"` + Code string `json:"code"` + Phone string `json:"phone"` +} +type PhoneVerificationResponse struct { + SessionId string `json:"sessionId"` + RefreshToken string `json:"refresh_token"` + Phone string `json:"phone"` + Name string `json:"name"` + Email string `json:"email"` +} + +// RefreshSession used to refresh session by RefreshToken. +func (nalogruClient *Client) RefreshSession(device *device.Device) error { client := createHttpClient() payload := RefreshRequest{ - ClientSecret: nalogruClient.device.ClientSecret, - RefreshToken: nalogruClient.device.RefreshToken, + ClientSecret: device.ClientSecret, + RefreshToken: device.RefreshToken, } resp, err := json.Marshal(payload) @@ -247,14 +325,14 @@ func (nalogruClient *Client) RefreshSession() error { url := nalogruClient.BaseAddress + "/v2/mobile/users/refresh" request, err := http.NewRequest(http.MethodPost, url, reader) - addHeaders(request, nalogruClient.device.Id.Hex()) + addHeaders(request, device.Id.Hex()) res, err := sendRequest(request, client) if err != nil { log.Printf("Can't POST %s\n", url) return err } - defer res.Body.Close() + defer dispose.Dispose(res.Body.Close, "Can't close HTTP response body") if res.StatusCode != http.StatusOK { log.Printf("Refresh session error: %d\n", res.StatusCode) @@ -268,9 +346,50 @@ func (nalogruClient *Client) RefreshSession() error { return err } log.Printf("%+v\n", response) - nalogruClient.device.RefreshToken = response.RefreshToken - nalogruClient.device.SessionId = response.SessionId - return nil + + return device.Update(response.SessionId, response.RefreshToken) +} + +func (nalogruClient *Client) VerifyPhone(device *device.Device, code string) error { + client := createHttpClient() + + payload := PhoneVerificationRequest{ + ClientSecret: device.ClientSecret, + Code: code, + Phone: device.Phone, + } + + resp, err := json.Marshal(payload) + if err != nil { + return err + } + reader := bytes.NewReader(resp) + + url := nalogruClient.BaseAddress + "/v2/auth/phone/verify" + request, err := http.NewRequest(http.MethodPost, url, reader) + addHeaders(request, device.Id.Hex()) + res, err := sendRequest(request, client) + + if err != nil { + log.Printf("Can't POST %s\n", url) + return err + } + defer dispose.Dispose(res.Body.Close, "Can't close HTTP response body") + + if res.StatusCode != http.StatusOK { + log.Printf("Phone verification error: %d\n", res.StatusCode) + return errors.New(fmt.Sprintf("HTTP error: %d", res.StatusCode)) + } + + response := &PhoneVerificationResponse{} + err = json.NewDecoder(res.Body).Decode(response) + if err != nil { + log.Println("Can't decode response body") + return err + } + log.Printf("%+v\n", response) + err = device.Update(response.SessionId, response.RefreshToken) + return err } func sendRequest(request *http.Request, client *http.Client) (*http.Response, error) { diff --git a/backend/nalogru/client_test.go b/backend/nalogru/client_test.go index 7a43916..4845767 100644 --- a/backend/nalogru/client_test.go +++ b/backend/nalogru/client_test.go @@ -5,6 +5,7 @@ import ( "log" "net/http" "net/http/httptest" + "receipt_collector/dispose" "receipt_collector/nalogru/device" "testing" ) @@ -12,18 +13,21 @@ import ( var baseAddress = "https://irkkt-mobile.nalog.ru:8888" var sessionId = "INSERT SESSION ID HERE" var deviceId = primitive.NewObjectID().Hex() +var secret = "INSERT SECRET HERE" +var refreshToken = "INSERT REFRESH TOKEN HERE" +var phone = "INSERT PHONE HERE" func IgnoreTestClient_GetTicketId(t *testing.T) { - d, err := createDevice("", "") + d, err := createDevice() if err != nil { log.Println(err) t.Fail() return } - client := NewClient(baseAddress, d) + client := NewClient(baseAddress) queryString := "INSERT BARCODE TEST HERE" - id, err := client.GetTicketId(queryString) + id, err := client.GetTicketId(queryString, d) if err != nil { log.Println(err) @@ -39,7 +43,36 @@ func IgnoreTestClient_GetTicketId(t *testing.T) { } -func createDevice(secret string, token string) (*device.Device, error) { +func IgnoreTestClient_GetElectronicTickets(t *testing.T) { + d, err := createDevice() + if err != nil { + log.Println(err) + t.Fail() + return + } + client := NewClient(baseAddress) + + tickets, err := client.GetElectronicTickets(d) + if err != nil { + log.Println(err) + t.Fail() + return + } + + if tickets == nil { + log.Println("Tickets not found") + t.Fail() + return + } + if len(tickets) == 0 { + log.Println("Tickets empty") + t.Fail() + return + } + +} + +func createDevice() (*device.Device, error) { id, err := primitive.ObjectIDFromHex(deviceId) if err != nil { return nil, err @@ -48,22 +81,26 @@ func createDevice(secret string, token string) (*device.Device, error) { SessionId: sessionId, Id: id, ClientSecret: secret, - RefreshToken: token, + RefreshToken: refreshToken, + Update: func(string, string) error { + return nil + }, + Phone: phone, } return d, err } func IgnoreTestClient_GetTicketById(t *testing.T) { - d, err := createDevice("", "") + d, err := createDevice() if err != nil { log.Println(err) t.Fail() return } - client := NewClient(baseAddress, d) + client := NewClient(baseAddress) ticketId := "INSERT TICKET ID HERE" - details, err := client.GetTicketById(ticketId) + details, err := client.GetTicketById(ticketId, d) if err != nil { log.Println(err) @@ -77,17 +114,15 @@ func IgnoreTestClient_GetTicketById(t *testing.T) { } func IgnoreTestClient_RefreshSession(t *testing.T) { - secret := "PASS CLIENT SECRET HERE" - refreshToken := "PASS REFRESH TOKEN HERE" - d, err := createDevice(secret, refreshToken) + d, err := createDevice() if err != nil { log.Println(err) t.Fail() return } - client := NewClient(baseAddress, d) + client := NewClient(baseAddress) - err = client.RefreshSession() + err = client.RefreshSession(d) if err != nil { log.Println(err) @@ -109,14 +144,14 @@ func IgnoreTestClient_RefreshSession(t *testing.T) { func IgnoreTestClient_CheckReceiptExist(t *testing.T) { queryString := "INSERT VALID QUERY STRING HERE" - d, err := createDevice("", "") + d, err := createDevice() if err != nil { log.Println(err) t.Fail() return } - client := NewClient(baseAddress, d) - exist, err := client.CheckReceiptExist(queryString) + client := NewClient(baseAddress) + exist, err := client.CheckReceiptExist(queryString, d) if err != nil { log.Println(err) @@ -161,6 +196,20 @@ func IgnoreTestHttpClient_Error(t *testing.T) { } } +func IgnoreTest_AuthByPhone(t *testing.T) { + d, err := createDevice() + if err != nil { + log.Println(err) + t.Fail() + return + } + client := NewClient(baseAddress) + err = client.AuthByPhone(d) + if err != nil { + t.Fatalf("Error auth by phone %v", err) + } +} + func createServer(t *testing.T) *httptest.Server { return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { _, err := w.Write([]byte("Hello")) @@ -174,7 +223,7 @@ func createServer(t *testing.T) *httptest.Server { func callServerWithCloseBody(client http.Client, svr *httptest.Server) error { m, err := client.Get(svr.URL) if m != nil { - defer m.Body.Close() + defer dispose.Dispose(m.Body.Close, "Can't close HTTP response body") } return err diff --git a/backend/nalogru/device/device.go b/backend/nalogru/device/device.go index 14ea1d7..6c2a295 100644 --- a/backend/nalogru/device/device.go +++ b/backend/nalogru/device/device.go @@ -3,8 +3,11 @@ package device import "go.mongodb.org/mongo-driver/bson/primitive" type Device struct { - ClientSecret string `bson:"client_secret"` - SessionId string `bson:"session_id"` - RefreshToken string `bson:"refresh_token"` - Id primitive.ObjectID `bson:"_id,omitempty"` + ClientSecret string `bson:"client_secret"` + SessionId string `bson:"session_id"` + RefreshToken string `bson:"refresh_token"` + Id primitive.ObjectID `bson:"_id,omitempty"` + Update func(sessionId string, refreshToken string) error `bson:"-"` + UserId string `bson:"user_id"` + Phone string `bson:"phone"` } diff --git a/backend/nalogru/devices.go b/backend/nalogru/devices.go index 30db2f8..3fecb41 100644 --- a/backend/nalogru/devices.go +++ b/backend/nalogru/devices.go @@ -6,9 +6,12 @@ import ( ) type Devices interface { - Add(ctx context.Context, d device.Device) error + Add(ctx context.Context, d *device.Device) error Count(ctx context.Context) (int, error) Rent(ctx context.Context) (*device.Device, error) - Update(ctx context.Context, device *device.Device) error + RentDevice(ctx context.Context, d *device.Device) error + Update(ctx context.Context, device *device.Device, sessionId string, refreshToken string) error Free(ctx context.Context, device *device.Device) error + All(ctx context.Context) []*device.Device + GetByUserId(ctx context.Context, userId string) (*device.Device, error) } diff --git a/backend/receipts/controller.go b/backend/receipts/controller.go index 969e38e..d1516df 100644 --- a/backend/receipts/controller.go +++ b/backend/receipts/controller.go @@ -10,21 +10,18 @@ import ( "net/http" "receipt_collector/auth" "receipt_collector/dispose" - "receipt_collector/nalogru" "receipt_collector/nalogru/qr" "strings" ) type Controller struct { - repository Repository - nalogruClient *nalogru.Client + repository Repository } // New creates controller. -func New(repository Repository, nalogruClient *nalogru.Client) Controller { +func New(repository Repository) Controller { return Controller{ - repository: repository, - nalogruClient: nalogruClient, + repository: repository, } } func onError(writer http.ResponseWriter, err error) { diff --git a/backend/receipts/repository.go b/backend/receipts/repository.go index d07a797..5f10512 100644 --- a/backend/receipts/repository.go +++ b/backend/receipts/repository.go @@ -110,7 +110,8 @@ func (repository *Repository) GetByQueryString(ctx context.Context, userId strin query := bson.D{{"owner", ownerId}, {"query_string", queryString}} result := collection.FindOne(ctx, query) - if result.Err() != nil { + err = result.Err() + if err != nil { return nil, err } @@ -121,6 +122,24 @@ func (repository *Repository) GetByQueryString(ctx context.Context, userId strin } +// GetAllOwnersByQueryString find user receipt by QR code query string without filtering by owner. +func (repository *Repository) GetAllOwnersByQueryString(ctx context.Context, queryString string) (*UsersReceipt, error) { + collection := repository.getCollection() + + query := bson.D{{"query_string", queryString}} + + result := collection.FindOne(ctx, query) + err := result.Err() + if err != nil { + return nil, err + } + + receipt := UsersReceipt{} + err = result.Decode(&receipt) + + return &receipt, err +} + // GetById returns receipt by it's id. func (repository *Repository) GetById(ctx context.Context, userId string, receiptId string) (UsersReceipt, error) { receipt := UsersReceipt{} diff --git a/backend/users/login_url/processor.go b/backend/users/login_url/processor.go deleted file mode 100644 index a178726..0000000 --- a/backend/users/login_url/processor.go +++ /dev/null @@ -1,74 +0,0 @@ -package login_url - -import ( - "context" - api "github.com/drypa/ReceiptCollector/api/inside" - "google.golang.org/grpc" - "receipt_collector/users" - "time" -) - -//Processor provides method to return login link. -type Processor struct { - repository *users.Repository - linkGenerator users.LinkGenerator -} - -//NewProcessor constructs Processor. -func NewProcessor(repository *users.Repository, linkGenerator users.LinkGenerator) *Processor { - return &Processor{repository: repository, linkGenerator: linkGenerator} -} - -//GetLoginLink returns login link for user in request. -func (p Processor) GetLoginLink(ctx context.Context, in *api.GetLoginLinkRequest) (*api.LoginLinkResponse, error) { - telegramId := in.TelegramId - user, err := p.repository.GetByTelegramId(ctx, int(telegramId)) - if err != nil { - return nil, err - } - url, err := p.linkGenerator.GetRedirectLink(user.Id.Hex()) - expiration := time.Now().Add(time.Minute * 15) - err = p.repository.UpdateLoginLink(ctx, user.Id, url, expiration) - if err != nil { - return nil, err - } - return &api.LoginLinkResponse{ - Url: url, - Expiration: expiration.Unix(), - }, nil -} - -func (p Processor) GetUsers(ctx context.Context, req *api.NoParams) (*api.GetUsersResponse, error) { - all, err := p.repository.GetAll(ctx) - if err != nil { - return nil, err - } - usersList := make([]*api.User, len(all)) - - for i, v := range all { - usersList[i] = &api.User{ - UserId: v.Id.Hex(), - TelegramId: int32(v.TelegramId), - } - } - - resp := api.GetUsersResponse{ - Users: usersList, - } - return &resp, err -} - -//GetUser get user by telegramId. -func (p Processor) GetUser(ctx context.Context, in *api.GetUserRequest, opts ...grpc.CallOption) (*api.GetUserResponse, error) { - user, err := p.repository.GetByTelegramId(ctx, int(in.TelegramId)) - if err != nil { - return nil, err - } - response := api.GetUserResponse{ - User: &api.User{ - UserId: user.Id.Hex(), - TelegramId: int32(user.TelegramId), - }, - } - return &response, err -} diff --git a/backend/users/processor.go b/backend/users/processor.go new file mode 100644 index 0000000..cb25189 --- /dev/null +++ b/backend/users/processor.go @@ -0,0 +1,164 @@ +package users + +import ( + "context" + api "github.com/drypa/ReceiptCollector/api/inside" + "google.golang.org/grpc" + "receipt_collector/device" + "receipt_collector/nalogru" + nalogDevice "receipt_collector/nalogru/device" + "time" +) + +// Processor provides method to return login link. +type Processor struct { + repository *Repository + linkGenerator LinkGenerator + deviceService *device.Service + nalogClient *nalogru.Client + clientSecret string +} + +// NewProcessor constructs Processor. +func NewProcessor(repository *Repository, linkGenerator LinkGenerator, nalogClient *nalogru.Client, d *device.Service, secret string) *Processor { + return &Processor{ + repository: repository, + linkGenerator: linkGenerator, + nalogClient: nalogClient, + deviceService: d, + clientSecret: secret} +} + +// GetLoginLink returns login link for user in request. +func (p Processor) GetLoginLink(ctx context.Context, in *api.GetLoginLinkRequest) (*api.LoginLinkResponse, error) { + telegramId := in.TelegramId + user, err := p.repository.GetByTelegramId(ctx, int(telegramId)) + if err != nil { + return nil, err + } + url, err := p.linkGenerator.GetRedirectLink(user.Id.Hex()) + expiration := time.Now().Add(time.Minute * 15) + err = p.repository.UpdateLoginLink(ctx, user.Id, url, expiration) + if err != nil { + return nil, err + } + return &api.LoginLinkResponse{ + Url: url, + Expiration: expiration.Unix(), + }, nil +} + +// GetUsers returns all registered users. +func (p Processor) GetUsers(ctx context.Context, _ *api.NoParams) (*api.GetUsersResponse, error) { + all, err := p.repository.GetAll(ctx) + if err != nil { + return nil, err + } + usersList := make([]*api.User, len(all)) + + for i, v := range all { + usersList[i] = &api.User{ + UserId: v.Id.Hex(), + TelegramId: int32(v.TelegramId), + } + } + + resp := api.GetUsersResponse{ + Users: usersList, + } + return &resp, err +} + +// GetUser get user by telegramId. +func (p Processor) GetUser(ctx context.Context, in *api.GetUserRequest, _ ...grpc.CallOption) (*api.GetUserResponse, error) { + user, err := p.repository.GetByTelegramId(ctx, int(in.TelegramId)) + if err != nil { + return nil, err + } + response := api.GetUserResponse{ + User: &api.User{ + UserId: user.Id.Hex(), + TelegramId: int32(user.TelegramId), + }, + } + return &response, err +} + +func (p Processor) RegisterUser(ctx context.Context, in *api.UserRegistrationRequest, _ ...grpc.CallOption) (*api.UserRegistrationResponse, error) { + user, err := p.repository.GetByTelegramId(ctx, int(in.TelegramId)) + if err != nil { + return nil, err + } + if user == nil { + user, err = p.addNewUser(ctx, int(in.TelegramId)) + if err != nil { + return nil, err + } + } + userId := user.Id.Hex() + d, err := p.deviceService.GetByUserId(ctx, userId) + if err != nil { + return nil, err + } + if d == nil { + err := p.addNewDevice(ctx, in.PhoneNumber, userId) + if err != nil { + return nil, err + } + } + err = p.nalogClient.AuthByPhone(d) + + return &api.UserRegistrationResponse{UserId: userId}, nil +} + +func (p Processor) addNewDevice(ctx context.Context, phoneNumber string, userId string) error { + d := &nalogDevice.Device{ + ClientSecret: p.clientSecret, + SessionId: "", + RefreshToken: "", + Update: nil, + UserId: userId, + Phone: phoneNumber, + } + err := p.deviceService.Add(ctx, d) + return err +} + +// VerifyPhone validate phone number through SMS. +func (p Processor) VerifyPhone(ctx context.Context, req *api.PhoneVerificationRequest) (*api.ErrorResponse, error) { + user, err := p.repository.GetByTelegramId(ctx, int(req.TelegramId)) + if err != nil { + return nil, err + } + if user == nil { + response := api.ErrorResponse{ + Error: "User not found", + } + return &response, nil + } + userId := user.Id.Hex() + d, err := p.deviceService.GetByUserId(ctx, userId) + if err != nil { + return nil, err + } + if d == nil { + response := api.ErrorResponse{ + Error: "Registration process not started", + } + return &response, nil + } + + err = p.nalogClient.VerifyPhone(d, req.Code) + return &api.ErrorResponse{Error: "TODO"}, err +} + +func (p Processor) addNewUser(ctx context.Context, telegramId int) (*User, error) { + u := User{ + TelegramId: telegramId, + } + err := p.repository.Insert(ctx, &u) + if err != nil { + return nil, err + } + return &u, nil +} diff --git a/backend/workers/electronic.go b/backend/workers/electronic.go new file mode 100644 index 0000000..63d016c --- /dev/null +++ b/backend/workers/electronic.go @@ -0,0 +1,68 @@ +package workers + +import ( + "context" + "github.com/go-co-op/gocron" + "go.mongodb.org/mongo-driver/bson/primitive" + "log" + "receipt_collector/nalogru" + "receipt_collector/nalogru/qr" + "receipt_collector/receipts" + "time" +) + +// GetElectronicReceiptStart receives electronic receipts for all devices. +func (worker *Worker) GetElectronicReceiptStart(ctx context.Context) { + if ctx.Err() != nil { + return + } + s := gocron.NewScheduler(time.Local) + + _, err := s.Every(1).Day().At("01:00").Do(worker.getElectronic, ctx) + if err != nil { + log.Printf("failed to create job %v\n", err) + } + + s.StartAsync() +} +func (worker *Worker) getElectronic(ctx context.Context) { + for _, d := range worker.devices.All(ctx) { + tickets, err := worker.nalogruClient.GetElectronicTickets(d) + if err != nil { + log.Printf("Failed to get electronic tickets: %v\n", err) + return + } + err = worker.insertTicketsIfNeeded(ctx, tickets) + if err != nil { + log.Printf("Failed to insert electronic tickets: %v\n", err) + return + } + } +} +func (worker *Worker) insertTicketsIfNeeded(ctx context.Context, tickets []*nalogru.TicketDetails) error { + for _, t := range tickets { + query, err := qr.Parse(t.Qr) + if err != nil { + log.Printf("Failed to parse '%s'\n", t.Qr) + return err + } + receipt, err := worker.repository.GetAllOwnersByQueryString(ctx, query.ToString()) + if err != nil { + log.Printf("Failed to get receipts by qr '%s'\n", t.Qr) + return err + } + if receipt == nil { + receiptRequest := receipts.UsersReceipt{ + Owner: primitive.NilObjectID, //TODO: owner needed + QueryString: query.ToString(), + //Without ticketId would process by get-worker + } + err := worker.repository.Insert(ctx, receiptRequest) + if err != nil { + log.Printf("Failed to insert receipt request for %s", query.ToString()) //TODO: add owner to output + return err + } + } + } + return nil +} diff --git a/backend/workers/get.go b/backend/workers/get.go index 7838274..3edea85 100644 --- a/backend/workers/get.go +++ b/backend/workers/get.go @@ -4,12 +4,13 @@ import ( "context" "log" "receipt_collector/nalogru" + "receipt_collector/nalogru/device" "receipt_collector/nalogru/qr" "receipt_collector/receipts" "time" ) -//GetReceiptStart starts get receipt worker. +// GetReceiptStart starts get receipt worker. func (worker *Worker) GetReceiptStart(ctx context.Context, settings Settings) { ticker := time.NewTicker(settings.Interval) @@ -67,21 +68,14 @@ func (worker *Worker) getReceipt(ctx context.Context) error { return err } normalizedQr := query.ToString() - id, err := worker.nalogruClient.GetTicketId(normalizedQr) + device, err := worker.devices.Rent(ctx) + defer worker.devices.Free(ctx, device) + id, err := worker.nalogruClient.GetTicketId(normalizedQr, device) if err != nil && err.Error() == nalogru.DailyLimitReached { return err } - if err == nalogru.AuthError { - err = worker.refreshSession(ctx) - if err != nil { - log.Printf("failed to refresh session. %v\n", err) - return err - } - id, err = worker.nalogruClient.GetTicketId(normalizedQr) - } - if err != nil { log.Printf("failed get receipt id %v\n", err) err := worker.repository.SetReceiptStatus(ctx, receipt.Id.Hex(), receipts.Error) @@ -94,20 +88,11 @@ func (worker *Worker) getReceipt(ctx context.Context) error { return err } - return worker.loadRawReceipt(ctx, id) + return worker.loadRawReceipt(ctx, id, device) } -func (worker *Worker) loadRawReceipt(ctx context.Context, id string) error { - details, err := worker.nalogruClient.GetTicketById(id) - - if err == nalogru.AuthError { - err = worker.refreshSession(ctx) - if err != nil { - log.Printf("failed to refresh session. %v\n", err) - return err - } - details, err = worker.nalogruClient.GetTicketById(id) - } +func (worker *Worker) loadRawReceipt(ctx context.Context, id string, device *device.Device) error { + details, err := worker.nalogruClient.GetTicketById(id, device) if err != nil { log.Printf("get ticket by id %s failed: %v", id, err) @@ -128,18 +113,18 @@ func getTicketExistence(details *nalogru.TicketDetails) string { return ticket } -func (worker *Worker) refreshSession(ctx context.Context) error { - err := worker.nalogruClient.RefreshSession() - if err != nil { - log.Printf("failed to refresh session: %v", err) - return err - } - device := worker.nalogruClient.GetDevice() - err = worker.devices.Update(ctx, &device) - if err != nil { - log.Printf("failed to update device: %v", err) - return err - } - log.Printf("device %s updated\n", device.Id.Hex()) - return nil -} +//func (worker *Worker) refreshSession(ctx context.Context) error { +// err := worker.nalogruClient.RefreshSession() +// if err != nil { +// log.Printf("failed to refresh session: %v", err) +// return err +// } +// device := worker.nalogruClient.GetDevice() +// err = worker.devices.Update(ctx, &device) +// if err != nil { +// log.Printf("failed to update device: %v", err) +// return err +// } +// log.Printf("device %s updated\n", device.Id.Hex()) +// return nil +//} diff --git a/backend/workers/update.go b/backend/workers/update.go index 1cf5144..98dc179 100644 --- a/backend/workers/update.go +++ b/backend/workers/update.go @@ -6,7 +6,7 @@ import ( "time" ) -//UpdateRawReceiptStart fetch tickets with wrong status. +// UpdateRawReceiptStart fetch tickets with wrong status. func (worker *Worker) UpdateRawReceiptStart(ctx context.Context, settings Settings) { ticker := time.NewTicker(settings.Interval) @@ -33,7 +33,7 @@ func (worker *Worker) UpdateRawReceiptStart(ctx context.Context, settings Settin if receipt == nil { break } - err = worker.loadRawReceipt(ctx, receipt.Id) + err = worker.loadRawReceipt(ctx, receipt.Id, nil) if err != nil { log.Printf("Failed to reload raw ticket. %v\n", err) } diff --git a/backend/workers/worker.go b/backend/workers/worker.go index 551d61a..371f382 100644 --- a/backend/workers/worker.go +++ b/backend/workers/worker.go @@ -6,7 +6,7 @@ import ( "receipt_collector/waste" ) -//Worker for any background job. +// Worker for any background job. type Worker struct { nalogruClient *nalogru.Client repository receipts.Repository @@ -14,10 +14,9 @@ type Worker struct { devices nalogru.Devices } -//New constructs Worker. +// New constructs Worker. func New(nalogruClient *nalogru.Client, repository receipts.Repository, wasteRepository *waste.Repository, devices nalogru.Devices) Worker { return Worker{ - //TODO: nalogruClient is not required here nalogruClient: nalogruClient, repository: repository, wasteRepository: wasteRepository, diff --git a/bot/backend/grpc.go b/bot/backend/grpc.go index fd95ed4..fb1547e 100644 --- a/bot/backend/grpc.go +++ b/bot/backend/grpc.go @@ -119,3 +119,25 @@ func (c *GrpcClient) GetReceiptReport(ctx context.Context, userId string, qr str return resp.Report, resp.FileName, err } + +func (c *GrpcClient) RegisterUser(ctx context.Context, telegramId int, phone string) error { + client := c.internal + request := inside.UserRegistrationRequest{ + TelegramId: int32(telegramId), + PhoneNumber: phone, + } + _, err := (*client).RegisterUser(ctx, &request) + + return err +} + +func (c *GrpcClient) VerifyPhone(ctx context.Context, telegramId int, code string) error { + client := c.internal + request := inside.PhoneVerificationRequest{ + TelegramId: int32(telegramId), + Code: code, + } + _, err := (*client).VerifyPhone(ctx, &request) + + return err +} diff --git a/bot/bot.go b/bot/bot.go index 655828c..3ca7a32 100644 --- a/bot/bot.go +++ b/bot/bot.go @@ -4,15 +4,14 @@ import ( "errors" "fmt" api "github.com/drypa/ReceiptCollector/api/inside" - "github.com/drypa/ReceiptCollector/bot/backend" "github.com/drypa/ReceiptCollector/bot/backend/report" - "github.com/drypa/ReceiptCollector/bot/backend/user" "github.com/drypa/ReceiptCollector/bot/commands" tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api" "log" "net/http" "net/url" "os" + "strings" ) func validateEmpty(value string, emptyErrorMessage string) error { @@ -31,11 +30,7 @@ func getEnvVar(varName string) string { return value } -func start(options Options, grpcClient *backend.GrpcClient, reportsClient *report.Client) error { - provider, err := user.New(grpcClient) - if err != nil { - return err - } +func start(options Options, reportsClient *report.Client, registrar *commands.Registrar) error { bot, err := create(options) if err != nil { log.Printf("Bot create error: %v\n", err) @@ -51,7 +46,7 @@ func start(options Options, grpcClient *backend.GrpcClient, reportsClient *repor return err } go processNotifications(bot, reportsClient.Notifications) - processUpdates(updatesChan, bot, grpcClient, provider) + processUpdates(updatesChan, bot, registrar) return nil } @@ -92,34 +87,25 @@ func create(options Options) (*tgbotapi.BotAPI, error) { func processUpdates(updatesChan tgbotapi.UpdatesChannel, bot *tgbotapi.BotAPI, - grpcClient *backend.GrpcClient, - provider user.Provider) { + registrar *commands.Registrar) { for update := range updatesChan { - log.Printf("%v\n", update) if update.Message == nil { continue } - processMessage(update, bot, provider, grpcClient) + + processMessage(update, bot, registrar) } } -func processMessage(update tgbotapi.Update, bot *tgbotapi.BotAPI, provider user.Provider, grpcClient *backend.GrpcClient) { - var err error - switch update.Message.Text { - case "": - err = commands.Empty(update, bot, err) - case "/start": - err = commands.Start(update, bot, err) - case "/register": - err = commands.Register(update, bot, provider) - case "/login": - err = commands.Login(update, bot, grpcClient) - case "/get": - err = commands.GetReceiptReport(update, bot, provider, grpcClient) - default: - err = commands.AddReceipt(update, bot, provider, grpcClient) - } - if err != nil { - log.Printf("Error while sending response to user %d", update.Message.From.ID) +func processMessage(update tgbotapi.Update, bot *tgbotapi.BotAPI, registrar *commands.Registrar) { + text := strings.TrimSpace(update.Message.Text) + c := registrar.Get(text) + if c != nil { + err := (*c).Execute(update, bot) + if err != nil { + log.Printf("Error while process request '%s' from %d. %v", text, update.Message.From.ID, err) + } + } + } diff --git a/bot/commands/add_receipt.go b/bot/commands/add_receipt.go new file mode 100644 index 0000000..f0a0b1e --- /dev/null +++ b/bot/commands/add_receipt.go @@ -0,0 +1,42 @@ +package commands + +import ( + "github.com/drypa/ReceiptCollector/bot/backend" + "github.com/drypa/ReceiptCollector/bot/backend/user" + tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api" + "strings" +) + +type AddReceiptCommand struct { + provider *user.Provider + grpcClient *backend.GrpcClient +} + +func NewAddReceiptCommand(provider *user.Provider, grpcClient *backend.GrpcClient) *AddReceiptCommand { + return &AddReceiptCommand{provider: provider, grpcClient: grpcClient} +} + +func (a AddReceiptCommand) Accepted(message string) bool { + return strings.Contains(message, "t=") && + strings.Contains(message, "s=") && + strings.Contains(message, "fn=") && + strings.Contains(message, "fp=") && + strings.Contains(message, "i=") && + strings.Contains(message, "n=") +} + +// Execute command +func (a AddReceiptCommand) Execute(update tgbotapi.Update, bot *tgbotapi.BotAPI) error { + id, err := a.provider.GetUserId(update.Message.From.ID) + if err != nil { + return err + } + err = tryAddReceipt(id, update.Message.Text, a.grpcClient) + + responseText := "Added" + if err != nil { + responseText = err.Error() + } + _, err = replyToMessage(update.Message.Chat.ID, bot, responseText, update.Message.MessageID) + return err +} diff --git a/bot/commands/code.go b/bot/commands/code.go new file mode 100644 index 0000000..c514031 --- /dev/null +++ b/bot/commands/code.go @@ -0,0 +1,50 @@ +package commands + +import ( + "context" + "github.com/drypa/ReceiptCollector/bot/backend" + "github.com/drypa/ReceiptCollector/bot/backend/user" + tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api" + "log" + "regexp" +) + +// ConfirmationCodeCommand is phone verification command +type ConfirmationCodeCommand struct { + provider *user.Provider + grpcClient *backend.GrpcClient + regexp *regexp.Regexp +} + +// NewConfirmationCodeCommand creates ConfirmationCodeCommand +func NewConfirmationCodeCommand(provider *user.Provider, grpcClient *backend.GrpcClient) *ConfirmationCodeCommand { + command := ConfirmationCodeCommand{ + provider: provider, + grpcClient: grpcClient, + regexp: regexp.MustCompile(`^/code\s+(?P\d{4})$`), + } + return &command +} + +func (c ConfirmationCodeCommand) Accepted(message string) bool { + return c.regexp.MatchString(message) +} + +// Execute command +func (c ConfirmationCodeCommand) Execute(update tgbotapi.Update, _ *tgbotapi.BotAPI) error { + userId, err := c.provider.GetUserId(update.Message.From.ID) + if err != nil { + return err + } + code := c.getCodeFromRequest(update.Message.Text) + log.Printf("User %s verify phone with code %s\n", userId, code) + + return c.grpcClient.VerifyPhone(context.Background(), update.Message.From.ID, code) +} + +func (c ConfirmationCodeCommand) getCodeFromRequest(message string) string { + matches := c.regexp.FindStringSubmatch(message) + index := c.regexp.SubexpIndex("code") + code := matches[index] + return code +} diff --git a/bot/commands/code_test.go b/bot/commands/code_test.go new file mode 100644 index 0000000..9c531b6 --- /dev/null +++ b/bot/commands/code_test.go @@ -0,0 +1,60 @@ +package commands + +import "testing" + +func TestAccepted_ConfirmationCodeCommand_Success(t *testing.T) { + command := NewConfirmationCodeCommand(nil, nil) + + variants := make([]string, 0) + variants = append(variants, "/code 1234") + variants = append(variants, "/code 9876") + variants = append(variants, "/code 0000") + variants = append(variants, "/code 0001") + + for _, s := range variants { + res := command.Accepted(s) + if !res { + t.Fatalf("message '%s' is not acceptable for ConfirmationCodeCommand", s) + } + } +} + +func TestAccepted_ConfirmationCodeCommand_Failed(t *testing.T) { + command := NewConfirmationCodeCommand(nil, nil) + + variants := make([]string, 0) + variants = append(variants, "") + variants = append(variants, " ") + variants = append(variants, " ") + variants = append(variants, "/code") + variants = append(variants, "code 1234") + variants = append(variants, "/code1234") + variants = append(variants, " /code 1234 ") + variants = append(variants, "/code +") + variants = append(variants, "/code ++++") + variants = append(variants, "/code dddd") + variants = append(variants, "/code 1") + variants = append(variants, "/code 12") + variants = append(variants, "/code 123") + variants = append(variants, "/code 12345") + variants = append(variants, "/code +.*") + + for _, s := range variants { + res := command.Accepted(s) + if res { + t.Fatalf("message '%s' is accepted for RegisterCommand", s) + } + } +} + +func Test_getCodeFromRequest_Success(t *testing.T) { + command := NewConfirmationCodeCommand(nil, nil) + + message := "/code 0101" + + phone := command.getCodeFromRequest(message) + expected := "0101" + if phone != expected { + t.Fatalf("wrong code returned from getCodeFromRequest. Expeced '%s' but got '%s'", expected, phone) + } +} diff --git a/bot/commands/common.go b/bot/commands/common.go index fec45f3..85eecfb 100644 --- a/bot/commands/common.go +++ b/bot/commands/common.go @@ -6,9 +6,9 @@ import ( "time" ) -func Empty(update tgbotapi.Update, bot *tgbotapi.BotAPI, err error) error { - _, err = sendTextMessage(update.Message.Chat.ID, bot, "Please enter a command.") - return err +type Command interface { + Accepted(message string) bool + Execute(update tgbotapi.Update, bot *tgbotapi.BotAPI) error } func sendTextMessage(chatId int64, bot *tgbotapi.BotAPI, responseText string) (tgbotapi.Message, error) { diff --git a/bot/commands/empty.go b/bot/commands/empty.go new file mode 100644 index 0000000..09dd4b3 --- /dev/null +++ b/bot/commands/empty.go @@ -0,0 +1,16 @@ +package commands + +import "github.com/go-telegram-bot-api/telegram-bot-api" + +type EmptyCommand struct { +} + +func (e EmptyCommand) Accepted(message string) bool { + return message == "" +} + +// Execute command +func (e EmptyCommand) Execute(update tgbotapi.Update, bot *tgbotapi.BotAPI) error { + _, err := sendTextMessage(update.Message.Chat.ID, bot, "Please enter a command.") + return err +} diff --git a/bot/commands/get_report.go b/bot/commands/get_report.go new file mode 100644 index 0000000..528b728 --- /dev/null +++ b/bot/commands/get_report.go @@ -0,0 +1,46 @@ +package commands + +import ( + "github.com/drypa/ReceiptCollector/bot/backend" + "github.com/drypa/ReceiptCollector/bot/backend/user" + "github.com/go-telegram-bot-api/telegram-bot-api" +) + +type GetReceiptReportCommand struct { + provider *user.Provider + grpcClient *backend.GrpcClient +} + +func NewGetReceiptReportCommand(provider *user.Provider, grpcClient *backend.GrpcClient) *GetReceiptReportCommand { + return &GetReceiptReportCommand{provider: provider, grpcClient: grpcClient} +} + +func (g GetReceiptReportCommand) Accepted(message string) bool { + return message == "/get" +} + +// Execute command +func (g GetReceiptReportCommand) Execute(update tgbotapi.Update, bot *tgbotapi.BotAPI) error { + id, err := g.provider.GetUserId(update.Message.From.ID) + if err != nil { + return err + } + qr, err := getQr(update) + if err != nil { + return err + } + + report, fileName, err := g.grpcClient.GetReceiptReport(getContext(), id, qr) + if err != nil { + return err + } + file := tgbotapi.FileBytes{ + Name: fileName, + Bytes: report, + } + upload := tgbotapi.NewDocumentUpload(update.Message.Chat.ID, file) + upload.ReplyToMessageID = update.Message.MessageID + _, err = bot.Send(upload) + + return err +} diff --git a/bot/commands/receipt.go b/bot/commands/receipt.go index c13a250..eab242b 100644 --- a/bot/commands/receipt.go +++ b/bot/commands/receipt.go @@ -3,52 +3,9 @@ package commands import ( "errors" "github.com/drypa/ReceiptCollector/bot/backend" - "github.com/drypa/ReceiptCollector/bot/backend/user" tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api" ) -// AddReceipt executes add new receipt by QR code command. -func AddReceipt(update tgbotapi.Update, bot *tgbotapi.BotAPI, provider user.Provider, grpcClient *backend.GrpcClient) error { - id, err := provider.GetUserId(update.Message.From.ID) - if err != nil { - return err - } - err = tryAddReceipt(id, update.Message.Text, grpcClient) - - responseText := "Added" - if err != nil { - responseText = err.Error() - } - _, err = replyToMessage(update.Message.Chat.ID, bot, responseText, update.Message.MessageID) - return err -} - -// GetReceiptReport is used to get receipt file by QR code. -func GetReceiptReport(update tgbotapi.Update, bot *tgbotapi.BotAPI, provider user.Provider, grpcClient *backend.GrpcClient) error { - id, err := provider.GetUserId(update.Message.From.ID) - if err != nil { - return err - } - qr, err := getQr(update) - if err != nil { - return err - } - - report, fileName, err := grpcClient.GetReceiptReport(getContext(), id, qr) - if err != nil { - return err - } - file := tgbotapi.FileBytes{ - Name: fileName, - Bytes: report, - } - upload := tgbotapi.NewDocumentUpload(update.Message.Chat.ID, file) - upload.ReplyToMessageID = update.Message.MessageID - _, err = bot.Send(upload) - - return err -} - func getQr(update tgbotapi.Update) (string, error) { if update.Message.ReplyToMessage != nil { return update.Message.ReplyToMessage.Text, nil diff --git a/bot/commands/register.go b/bot/commands/register.go new file mode 100644 index 0000000..69e29a4 --- /dev/null +++ b/bot/commands/register.go @@ -0,0 +1,50 @@ +package commands + +import ( + "context" + "github.com/drypa/ReceiptCollector/bot/backend" + "github.com/drypa/ReceiptCollector/bot/backend/user" + tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api" + "regexp" +) + +type RegisterCommand struct { + provider *user.Provider + regexp *regexp.Regexp + grpcClient *backend.GrpcClient +} + +func NewRegisterCommand(provider *user.Provider, grpcClient *backend.GrpcClient) *RegisterCommand { + return &RegisterCommand{ + provider: provider, + grpcClient: grpcClient, + regexp: regexp.MustCompile(`^/register\s+(?P\+\d{11})$`), + } +} + +func (r RegisterCommand) Accepted(message string) bool { + return r.regexp.MatchString(message) +} + +// Execute command +func (r RegisterCommand) Execute(update tgbotapi.Update, bot *tgbotapi.BotAPI) error { + phone := r.getPhoneFromRequest(update.Message.Text) + err := r.register(update.Message.From.ID, phone) + responseText := "You are registered.\n Wait SMS from KKT.NALOG and please send the verification code from it as reply to current message" + if err != nil { + responseText = err.Error() + } + _, err = sendTextMessage(update.Message.Chat.ID, bot, responseText) + return err +} + +func (r RegisterCommand) getPhoneFromRequest(message string) string { + matches := r.regexp.FindStringSubmatch(message) + index := r.regexp.SubexpIndex("phone") + phone := matches[index] + return phone +} + +func (r RegisterCommand) register(telegramId int, phone string) error { + return r.grpcClient.RegisterUser(context.Background(), telegramId, phone) +} diff --git a/bot/commands/register_test.go b/bot/commands/register_test.go new file mode 100644 index 0000000..47980f1 --- /dev/null +++ b/bot/commands/register_test.go @@ -0,0 +1,59 @@ +package commands + +import "testing" + +func TestAccepted_RegisterCommand_Success(t *testing.T) { + command := NewRegisterCommand(nil, nil) + + variants := make([]string, 0) + variants = append(variants, "/register +71234567890") + variants = append(variants, "/register +70987654321") + variants = append(variants, "/register +00987654321") + variants = append(variants, "/register +00987654321") + + for _, s := range variants { + res := command.Accepted(s) + if !res { + t.Fatalf("message '%s' is not acceptable for RegisterCommand", s) + } + } + +} + +func TestAccepted_RegisterCommand_Failed(t *testing.T) { + command := NewRegisterCommand(nil, nil) + + variants := make([]string, 0) + variants = append(variants, "") + variants = append(variants, " ") + variants = append(variants, " ") + variants = append(variants, "/register") + variants = append(variants, "register +71234567890") + variants = append(variants, "/register+71234567890") + variants = append(variants, " /register +11234567890 ") + variants = append(variants, "/register +") + variants = append(variants, "/register +1") + variants = append(variants, "/register +012345678901") + variants = append(variants, "/register +abcdefghjk") + variants = append(variants, "/register ++++++++++++") + variants = append(variants, "/register +.*") + + for _, s := range variants { + res := command.Accepted(s) + if res { + t.Fatalf("message '%s' is accepted for RegisterCommand", s) + } + } +} + +func Test_getPhoneFromRequest_Success(t *testing.T) { + command := NewRegisterCommand(nil, nil) + + message := "/register +71234567890" + + phone := command.getPhoneFromRequest(message) + expected := "+71234567890" + if phone != expected { + t.Fatalf("wrong phone returned from getPhoneFromRequest. Expeced '%s' but got '%s'", expected, phone) + } +} diff --git a/bot/commands/registrar.go b/bot/commands/registrar.go new file mode 100644 index 0000000..24afe61 --- /dev/null +++ b/bot/commands/registrar.go @@ -0,0 +1,30 @@ +package commands + +type Registrar struct { + commands []Command + defaultCommand Command +} + +func NewRegistrar() *Registrar { + registrar := Registrar{ + commands: []Command{}, + } + return ®istrar +} + +func (r *Registrar) Register(c Command) { + r.commands = append(r.commands, c) +} + +func (r *Registrar) RegisterDefault(c Command) { + r.defaultCommand = c +} + +func (r *Registrar) Get(message string) *Command { + for _, c := range r.commands { + if c.Accepted(message) { + return &c + } + } + return &r.defaultCommand +} diff --git a/bot/commands/registration.go b/bot/commands/registration.go deleted file mode 100644 index b168358..0000000 --- a/bot/commands/registration.go +++ /dev/null @@ -1,26 +0,0 @@ -package commands - -import ( - "github.com/drypa/ReceiptCollector/bot/backend/user" - tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api" -) - -func Start(update tgbotapi.Update, bot *tgbotapi.BotAPI, err error) error { - _, err = sendTextMessage(update.Message.Chat.ID, bot, "I'm a bot to collect Your purchase tickets.") - return err -} - -func Register(update tgbotapi.Update, bot *tgbotapi.BotAPI, provider user.Provider) error { - err := register(update.Message.From.ID, provider) - responseText := "You are registered." - if err != nil { - responseText = err.Error() - } - _, err = sendTextMessage(update.Message.Chat.ID, bot, responseText) - return err -} - -func register(userId int, client user.Provider) error { - _, err := client.GetUserId(userId) - return err -} diff --git a/bot/commands/start.go b/bot/commands/start.go new file mode 100644 index 0000000..9bb9fef --- /dev/null +++ b/bot/commands/start.go @@ -0,0 +1,16 @@ +package commands + +import "github.com/go-telegram-bot-api/telegram-bot-api" + +type StartCommand struct { +} + +func (s StartCommand) Accepted(message string) bool { + return message == "/start" +} + +// Execute command +func (s StartCommand) Execute(update tgbotapi.Update, bot *tgbotapi.BotAPI) error { + _, err := sendTextMessage(update.Message.Chat.ID, bot, "I'm a bot to collect Your purchase tickets.") + return err +} diff --git a/bot/commands/wrong.go b/bot/commands/wrong.go new file mode 100644 index 0000000..d75e546 --- /dev/null +++ b/bot/commands/wrong.go @@ -0,0 +1,16 @@ +package commands + +import "github.com/go-telegram-bot-api/telegram-bot-api" + +type WrongCommand struct { +} + +func (w WrongCommand) Accepted(_ string) bool { + return true +} + +// Execute command +func (w WrongCommand) Execute(update tgbotapi.Update, bot *tgbotapi.BotAPI) error { + _, err := sendTextMessage(update.Message.Chat.ID, bot, "Command not recognized.") + return err +} diff --git a/bot/go.mod b/bot/go.mod index 07b8e1c..dd234fc 100644 --- a/bot/go.mod +++ b/bot/go.mod @@ -3,17 +3,17 @@ module github.com/drypa/ReceiptCollector/bot go 1.20 require ( - github.com/drypa/ReceiptCollector/api/inside v0.0.0-20230411193413-50c7f98f123e + github.com/drypa/ReceiptCollector/api/inside v0.0.0-20230819122429-dfb6fd2c5a01 github.com/go-telegram-bot-api/telegram-bot-api v4.6.4+incompatible - google.golang.org/grpc v1.54.0 + google.golang.org/grpc v1.57.0 ) require ( github.com/golang/protobuf v1.5.3 // indirect github.com/technoweenie/multipartstreamer v1.0.1 // indirect - golang.org/x/net v0.9.0 // indirect - golang.org/x/sys v0.7.0 // indirect - golang.org/x/text v0.9.0 // indirect - google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1 // indirect - google.golang.org/protobuf v1.30.0 // indirect + golang.org/x/net v0.14.0 // indirect + golang.org/x/sys v0.11.0 // indirect + golang.org/x/text v0.12.0 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20230815205213-6bfd019c3878 // indirect + google.golang.org/protobuf v1.31.0 // indirect ) diff --git a/bot/go.sum b/bot/go.sum index 2a8b159..a70541a 100644 --- a/bot/go.sum +++ b/bot/go.sum @@ -1,7 +1,5 @@ -github.com/drypa/ReceiptCollector/api/inside v0.0.0-20230401140024-d19b1b654eb3 h1:NxegEtlE7ZXqJZKB3PhvWZW5sVjUenyuPeCQd4k3Ex4= -github.com/drypa/ReceiptCollector/api/inside v0.0.0-20230401140024-d19b1b654eb3/go.mod h1:4ULoAoegdVc7I/NgQ6izbBbXhqheKj8JbANBQ3xfVN0= -github.com/drypa/ReceiptCollector/api/inside v0.0.0-20230411193413-50c7f98f123e h1:C6WARgk3MZfpEnmGfQ1bK/YZnLP9tRLLOf5+Fi657Eg= -github.com/drypa/ReceiptCollector/api/inside v0.0.0-20230411193413-50c7f98f123e/go.mod h1:4ULoAoegdVc7I/NgQ6izbBbXhqheKj8JbANBQ3xfVN0= +github.com/drypa/ReceiptCollector/api/inside v0.0.0-20230819122429-dfb6fd2c5a01 h1:HCdDtLl5QNclGP45Q6dt9d5XEfA5OMf+WRdLUOqiJx4= +github.com/drypa/ReceiptCollector/api/inside v0.0.0-20230819122429-dfb6fd2c5a01/go.mod h1:4ULoAoegdVc7I/NgQ6izbBbXhqheKj8JbANBQ3xfVN0= github.com/go-telegram-bot-api/telegram-bot-api v4.6.4+incompatible h1:2cauKuaELYAEARXRkq2LrJ0yDDv1rW7+wrTEdVL3uaU= github.com/go-telegram-bot-api/telegram-bot-api v4.6.4+incompatible/go.mod h1:qf9acutJ8cwBUhm1bqgz6Bei9/C/c93FPDljKWwsOgM= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= @@ -11,26 +9,18 @@ github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= github.com/technoweenie/multipartstreamer v1.0.1 h1:XRztA5MXiR1TIRHxH2uNxXxaIkKQDeX7m2XsSOlQEnM= github.com/technoweenie/multipartstreamer v1.0.1/go.mod h1:jNVxdtShOxzAsukZwTSw6MDx5eUJoiEBsSvzDU9uzog= -golang.org/x/net v0.8.0 h1:Zrh2ngAOFYneWTAIAPethzeaQLuHwhuBkuV6ZiRnUaQ= -golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= -golang.org/x/net v0.9.0 h1:aWJ/m6xSmxWBx+V0XRHTlrYrPG56jKsLdTFmsSsCzOM= -golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns= -golang.org/x/sys v0.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ= -golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.7.0 h1:3jlCCIQZPdOYu1h8BkNvLz8Kgwtae2cagcG/VamtZRU= -golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/text v0.8.0 h1:57P1ETyNKtuIjB4SRd15iJxuhj8Gc416Y78H3qgMh68= -golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= -golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE= -golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/net v0.14.0 h1:BONx9s002vGdD9umnlX1Po8vOZmrgH34qlHcD1MfK14= +golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI= +golang.org/x/sys v0.11.0 h1:eG7RXZHdqOJ1i+0lgLgCpSXAp6M3LYlAo6osgSi0xOM= +golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/text v0.12.0 h1:k+n5B8goJNdU7hSvEtMUz3d1Q6D/XW4COJSJR6fN0mc= +golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -google.golang.org/genproto v0.0.0-20230331144136-dcfb400f0633 h1:0BOZf6qNozI3pkN3fJLwNubheHJYHhMh91GRFOWWK08= -google.golang.org/genproto v0.0.0-20230331144136-dcfb400f0633/go.mod h1:UUQDJDOlWu4KYeJZffbWgBkS1YFobzKbLVfK69pe0Ak= -google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1 h1:KpwkzHKEF7B9Zxg18WzOa7djJ+Ha5DzthMyZYQfEn2A= -google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1/go.mod h1:nKE/iIaLqn2bQwXBg8f1g2Ylh6r5MN5CmZvuzZCgsCU= -google.golang.org/grpc v1.54.0 h1:EhTqbhiYeixwWQtAEZAxmV9MGqcjEU2mFx52xCzNyag= -google.golang.org/grpc v1.54.0/go.mod h1:PUSEXI6iWghWaB6lXM4knEgpJNu2qUcKfDtNci3EC2g= +google.golang.org/genproto/googleapis/rpc v0.0.0-20230815205213-6bfd019c3878 h1:lv6/DhyiFFGsmzxbsUUTOkN29II+zeWHxvT8Lpdxsv0= +google.golang.org/genproto/googleapis/rpc v0.0.0-20230815205213-6bfd019c3878/go.mod h1:+Bk1OCOj40wS2hwAMA+aCW9ypzm63QTBBHp6lQ3p+9M= +google.golang.org/grpc v1.57.0 h1:kfzNeI/klCGD2YPMUlaGNT3pxvYfga7smW3Vth8Zsiw= +google.golang.org/grpc v1.57.0/go.mod h1:Sd+9RMTACXwmub0zcNY2c4arhtrbBYD1AUHI/dt16Mo= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= -google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng= -google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8= +google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= diff --git a/bot/main.go b/bot/main.go index 791f696..ec51178 100644 --- a/bot/main.go +++ b/bot/main.go @@ -3,6 +3,8 @@ package main import ( "github.com/drypa/ReceiptCollector/bot/backend" "github.com/drypa/ReceiptCollector/bot/backend/report" + "github.com/drypa/ReceiptCollector/bot/backend/user" + "github.com/drypa/ReceiptCollector/bot/commands" "google.golang.org/grpc/credentials" "log" "os" @@ -19,8 +21,40 @@ func main() { } grpcClient := backend.NewGrpcClient(backendGrpcAddress, creds) reportsClient := report.New(reportsGrpcAddress, creds) - err = start(options, grpcClient, reportsClient) + provider, err := user.New(grpcClient) if err != nil { log.Fatal(err) } + registrar := createCommandsRegistrar(grpcClient, &provider) + err = start(options, reportsClient, registrar) + if err != nil { + log.Fatal(err) + } +} + +func createCommandsRegistrar(grpcClient *backend.GrpcClient, users *user.Provider) *commands.Registrar { + registrar := commands.Registrar{} + + empty := commands.EmptyCommand{} + registrar.Register(empty) + + start := commands.StartCommand{} + registrar.Register(start) + + register := commands.NewRegisterCommand(users, grpcClient) + registrar.Register(register) + + code := commands.NewConfirmationCodeCommand(users, grpcClient) + registrar.Register(code) + + getReceiptReport := commands.NewGetReceiptReportCommand(users, grpcClient) + registrar.Register(getReceiptReport) + + addReceiptCommand := commands.NewAddReceiptCommand(users, grpcClient) + registrar.Register(addReceiptCommand) + + wrongCommand := commands.WrongCommand{} + registrar.RegisterDefault(wrongCommand) + + return ®istrar }