diff --git a/.golangci.yml b/.golangci.yml new file mode 100644 index 0000000..00da227 --- /dev/null +++ b/.golangci.yml @@ -0,0 +1,15 @@ +linters: + enable: + - revive + +linters-settings: + revive: + rules: + - name: exported + arguments: + - disableStutteringCheck + +issues: + include: + - EXC0012 + - EXC0014 \ No newline at end of file diff --git a/internal/config/config.go b/internal/config/config.go index 72801f1..fc88178 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -8,6 +8,7 @@ import ( "github.com/ilyakaznacheev/cleanenv" ) +// Config represents the configuration for the application. type Config struct { HTTPServer `yaml:"http_server"` Database `yaml:"database"` @@ -15,12 +16,14 @@ type Config struct { LogLevel string `yaml:"log_level" env-default:"Info" env:"LOG_LEVEL"` } +// HTTPServer represents the configuration for the HTTP server. type HTTPServer struct { Address string `yaml:"address" env-default:"localhost:8080"` Timeout time.Duration `yaml:"timeout" env-default:"4s" env:"TIMEOUT"` IdleTimeout time.Duration `yaml:"idle_timeout" env-default:"60s" env:"IDLE_TIMEOUT"` } +// Database represents the configuration for the PostgreSQL database. type Database struct { Host string `yaml:"host" env-default:"localhost" env:"DB_HOST"` Port int64 `yaml:"port" env-default:"5432" env:"DB_PORT"` @@ -29,10 +32,15 @@ type Database struct { Password string `yaml:"password" env-required:"true" env:"DB_PASSWORD"` } +// TelegramBot represents the configuration for the Telegram bot. type TelegramBot struct { Token string `yaml:"token" env-required:"true" env:"BOT_TOKEN"` } +// MustLoad loads the configuration from the file specified in the CONFIG_PATH environment variable. +// It returns a pointer to the loaded Config struct. +// If CONFIG_PATH is not set or the file does not exist, it logs a fatal error. +// If there is an error reading the config file, it logs a fatal error. func MustLoad() *Config { configPath := os.Getenv("CONFIG_PATH") if configPath == "" { diff --git a/internal/entities/chat.go b/internal/entities/chat.go index 9d247a1..d525cf4 100644 --- a/internal/entities/chat.go +++ b/internal/entities/chat.go @@ -1,7 +1,8 @@ package entities +// Chat represents a chat entity. type Chat struct { - Id int64 `db:"id"` - CompanyId int64 `db:"company_id"` - TelegramId int64 `db:"telegram_id"` + Id int64 `db:"id"` + CompanyID int64 `db:"company_id"` + TelegramID int64 `db:"telegram_id"` } diff --git a/internal/entities/company.go b/internal/entities/company.go index 3abded4..d52833d 100644 --- a/internal/entities/company.go +++ b/internal/entities/company.go @@ -1,22 +1,25 @@ package entities +// Company represents a company entity. type Company struct { - Id int64 `db:"id"` - OwnerId int64 `db:"owner_id"` + ID int64 `db:"id"` + OwnerID int64 `db:"owner_id"` Token string `db:"token"` Name string `db:"name"` Email string `db:"email"` } +// CompanyRegistrationInfo represents the information needed to register a new company. type CompanyRegistrationInfo struct { Name string Email string Owner OwnerInfo } +// CompanyInfo represents the information about a company. type CompanyInfo struct { - Id int64 - OwnerId int64 + ID int64 + OwnerID int64 Token string Name string Email string diff --git a/internal/entities/message.go b/internal/entities/message.go index 357aa0e..01f13ed 100644 --- a/internal/entities/message.go +++ b/internal/entities/message.go @@ -2,6 +2,7 @@ package entities import "strings" +// Message represents a message to be sent to one or more chats. type Message struct { Text string ParseMode ParseMode @@ -9,6 +10,7 @@ type Message struct { ChatIds []int64 } +// ParseMode represents the parsing mode for a message. type ParseMode string var ( @@ -20,12 +22,17 @@ var ( ) const ( + // Undefined represents an undefined parsing mode. Undefined ParseMode = "Undefined" + // MarkdownV2 represents the MarkdownV2 parsing mode. MarkdownV2 ParseMode = "MarkdownV2" + // Markdown represents the Markdown parsing mode. Markdown ParseMode = "Markdown" + // HTML represents the HTML parsing mode. HTML ParseMode = "HTML" ) +// String returns the string representation of the ParseMode. func (pm ParseMode) String() string { switch pm { case MarkdownV2: @@ -39,6 +46,7 @@ func (pm ParseMode) String() string { } } +// ParseString returns the ParseMode for the given string. func ParseString(str string) ParseMode { c, ok := parseModeMap[strings.ToLower(str)] if !ok { diff --git a/internal/entities/owner.go b/internal/entities/owner.go index feea956..1a205db 100644 --- a/internal/entities/owner.go +++ b/internal/entities/owner.go @@ -1,12 +1,14 @@ package entities +// Owner represents the owner of a company. type Owner struct { - Id int64 `db:"id"` - TelegramId int64 `db:"telegram_id"` + ID int64 `db:"id"` + TelegramID int64 `db:"telegram_id"` TelegramName string `db:"telegram_name"` } +// OwnerInfo represents information about the owner of a company. type OwnerInfo struct { - TelegramId int64 + TelegramID int64 TelegramName string } diff --git a/internal/lib/handlers/response.go b/internal/lib/handlers/response.go index a5d560a..af4ddd0 100644 --- a/internal/lib/handlers/response.go +++ b/internal/lib/handlers/response.go @@ -9,18 +9,21 @@ import ( "github.com/go-playground/validator/v10" ) +// ErrorResponse represents an error response returned by the API. type ErrorResponse struct { Message string `json:"message"` } -// nolint +// NewErrorResponse writes an error response to the provided http.ResponseWriter with the given status code and error message. func NewErrorResponse(w http.ResponseWriter, status int, err string) { w.WriteHeader(status) + // nolint:errcheck json.NewEncoder(w).Encode(ErrorResponse{ Message: err, }) } +// ValidationError generates a string message from the provided validation errors. func ValidationError(errs validator.ValidationErrors) string { var errMsgs []string diff --git a/internal/lib/logger/sl/sl.go b/internal/lib/logger/sl/sl.go index 3e30954..d2fb56e 100644 --- a/internal/lib/logger/sl/sl.go +++ b/internal/lib/logger/sl/sl.go @@ -4,9 +4,10 @@ import ( "golang.org/x/exp/slog" ) +// Err returns a slog.Attr with the given error message as its value and "error" as its key. func Err(err error) slog.Attr { return slog.Attr{ Key: "error", Value: slog.StringValue(err.Error()), } -} \ No newline at end of file +} diff --git a/internal/lib/logger/slogdiscard/slogdiscard.go b/internal/lib/logger/slogdiscard/slogdiscard.go index 6754b09..3e35a4f 100644 --- a/internal/lib/logger/slogdiscard/slogdiscard.go +++ b/internal/lib/logger/slogdiscard/slogdiscard.go @@ -6,32 +6,35 @@ import ( "golang.org/x/exp/slog" ) +// NewDiscardLogger creates a new logger that discards all log records. func NewDiscardLogger() *slog.Logger { return slog.New(NewDiscardHandler()) } +// DiscardHandler is a logger handler that discards all log records. type DiscardHandler struct{} +// NewDiscardHandler creates a new DiscardHandler instance. func NewDiscardHandler() *DiscardHandler { return &DiscardHandler{} } +// Handle ignores the log record. func (h *DiscardHandler) Handle(_ context.Context, _ slog.Record) error { - // Просто игнорируем запись журнала return nil } +// WithAttrs returns the handler itself since DiscardHandler does not use attributes. func (h *DiscardHandler) WithAttrs(_ []slog.Attr) slog.Handler { - // Возвращает тот же обработчик, так как нет атрибутов для сохранения return h } +// WithGroup returns the handler itself since DiscardHandler does not use groups. func (h *DiscardHandler) WithGroup(_ string) slog.Handler { - // Возвращает тот же обработчик, так как нет группы для сохранения return h } +// Enabled returns false since DiscardHandler does not enable any log levels. func (h *DiscardHandler) Enabled(_ context.Context, _ slog.Level) bool { - // Всегда возвращает false, так как запись журнала игнорируется return false } diff --git a/internal/lib/random/random.go b/internal/lib/random/random.go index 629bf77..b0916c3 100644 --- a/internal/lib/random/random.go +++ b/internal/lib/random/random.go @@ -19,4 +19,4 @@ func NewRandomString(size int) string { } return string(b) -} \ No newline at end of file +} diff --git a/internal/lib/validator/validator.go b/internal/lib/validator/validator.go index d4ef78d..9f8b804 100644 --- a/internal/lib/validator/validator.go +++ b/internal/lib/validator/validator.go @@ -10,6 +10,8 @@ var ( parseMode = []string{"html"} //"markdownv2", "markdown", ) +// ValidateParseMode checks if the provided parse mode is valid. +// Returns true if the parse mode is valid, false otherwise. func ValidateParseMode(fl validator.FieldLevel) bool { value := fl.Field().String() diff --git a/internal/storage/postgres/chat/chat.go b/internal/storage/postgres/chat/chat.go index 638cd03..3dde3a3 100644 --- a/internal/storage/postgres/chat/chat.go +++ b/internal/storage/postgres/chat/chat.go @@ -11,10 +11,12 @@ import ( "github.com/testit-tms/webhook-bot/internal/storage" ) +// ChatStorage is a storage implementation for chats using PostgreSQL. type ChatStorage struct { db *sqlx.DB } +// New returns a new instance of ChatStorage with the given database connection. func New(db *sqlx.DB) *ChatStorage { return &ChatStorage{ db: db, @@ -29,6 +31,7 @@ const ( deleteChatByCompanyId = "DELETE FROM chats WHERE company_id=$1" ) +// GetChatsByCompanyId returns a slice of entities.Chat that belong to the company with the given ID. func (s *ChatStorage) GetChatsByCompanyId(ctx context.Context, id int64) ([]entities.Chat, error) { const op = "storage.postgres.GetChatByCompanyId" @@ -45,6 +48,7 @@ func (s *ChatStorage) GetChatsByCompanyId(ctx context.Context, id int64) ([]enti return chats, nil } +// GetChatsByCompanyToken returns a slice of entities.Chat that belong to the company with the given token. func (s *ChatStorage) GetChatsByCompanyToken(ctx context.Context, t string) ([]entities.Chat, error) { const op = "storage.postgres.GetChatsByCompanyToken" @@ -61,12 +65,13 @@ func (s *ChatStorage) GetChatsByCompanyToken(ctx context.Context, t string) ([]e return chats, nil } +// AddChat adds a new chat to the database and returns the newly created chat entity. func (r *ChatStorage) AddChat(ctx context.Context, chat entities.Chat) (entities.Chat, error) { const op = "storage.postgres.AddChat" newChat := entities.Chat{} - err := r.db.QueryRowxContext(ctx, addChat, chat.CompanyId, chat.TelegramId).StructScan(&newChat) + err := r.db.QueryRowxContext(ctx, addChat, chat.CompanyID, chat.TelegramID).StructScan(&newChat) if err != nil { return newChat, fmt.Errorf("%s: execute query: %w", op, err) } @@ -74,6 +79,7 @@ func (r *ChatStorage) AddChat(ctx context.Context, chat entities.Chat) (entities return newChat, nil } +// DeleteChatById deletes a chat from the database by its ID. func (r *ChatStorage) DeleteChatById(ctx context.Context, id int64) error { const op = "storage.postgres.DeleteChatById" @@ -85,6 +91,7 @@ func (r *ChatStorage) DeleteChatById(ctx context.Context, id int64) error { return nil } +// DeleteChatByCompanyId deletes all chats from the database that belong to the company with the given ID. func (r *ChatStorage) DeleteChatByCompanyId(ctx context.Context, id int) error { const op = "storage.postgres.DeleteChatByCompanyId" diff --git a/internal/storage/postgres/chat/chat_test.go b/internal/storage/postgres/chat/chat_test.go index 5f2dcdc..0c21190 100644 --- a/internal/storage/postgres/chat/chat_test.go +++ b/internal/storage/postgres/chat/chat_test.go @@ -25,13 +25,13 @@ func TestChatStorage_GetChatsByCompanyId(t *testing.T) { chatsExp := []entities.Chat{ { Id: 12, - CompanyId: id, - TelegramId: 123456, + CompanyID: id, + TelegramID: 123456, }, { Id: 13, - CompanyId: id, - TelegramId: 654321, + CompanyID: id, + TelegramID: 654321, }, } @@ -106,13 +106,13 @@ func TestChatStorage_GetChatsByCompanyToken(t *testing.T) { chatsExp := []entities.Chat{ { Id: 12, - CompanyId: 21, - TelegramId: 123456, + CompanyID: 21, + TelegramID: 123456, }, { Id: 13, - CompanyId: 21, - TelegramId: 654321, + CompanyID: 21, + TelegramID: 654321, }, } @@ -184,14 +184,14 @@ func TestChatStorage_AddChat(t *testing.T) { defer f.Teardown() expectedChat := entities.Chat{ Id: 12, - CompanyId: 21, - TelegramId: 123456, + CompanyID: 21, + TelegramID: 123456, } rows := sqlmock.NewRows([]string{"id", "company_id", "telegram_id"}). AddRow(12, 21, "123456") f.Mock.ExpectQuery(regexp.QuoteMeta("INSERT INTO chats (company_id, telegram_id) VALUES ($1, $2) RETURNING id, company_id, telegram_id")). - WithArgs(expectedChat.CompanyId, expectedChat.TelegramId). + WithArgs(expectedChat.CompanyID, expectedChat.TelegramID). WillReturnRows(rows) repo := New(f.DB) @@ -213,12 +213,12 @@ func TestChatStorage_AddChat(t *testing.T) { expectErr := errors.New("test error") expectedChat := entities.Chat{ Id: 12, - CompanyId: 21, - TelegramId: 123456, + CompanyID: 21, + TelegramID: 123456, } f.Mock.ExpectQuery(regexp.QuoteMeta("INSERT INTO chats (company_id, telegram_id) VALUES ($1, $2) RETURNING id, company_id, telegram_id")). - WithArgs(expectedChat.CompanyId, expectedChat.TelegramId). + WithArgs(expectedChat.CompanyID, expectedChat.TelegramID). WillReturnError(expectErr) repo := New(f.DB) diff --git a/internal/storage/postgres/company/company.go b/internal/storage/postgres/company/company.go index 708a3a0..3b7e155 100644 --- a/internal/storage/postgres/company/company.go +++ b/internal/storage/postgres/company/company.go @@ -11,10 +11,12 @@ import ( "github.com/testit-tms/webhook-bot/internal/storage" ) +// CompanyStorage is a storage implementation for companies using PostgreSQL. type CompanyStorage struct { db *sqlx.DB } +// New creates a new instance of CompanyStorage with the given database connection. func New(db *sqlx.DB) *CompanyStorage { return &CompanyStorage{ db: db, @@ -27,12 +29,13 @@ const ( getCompanyIdByName = "SELECT id FROM companies WHERE name=$1" ) +// AddCompany adds a new company to the database and returns the newly created company. func (s *CompanyStorage) AddCompany(ctx context.Context, company entities.Company) (entities.Company, error) { const op = "storage.postgres.AddCompany" newCompany := entities.Company{} - err := s.db.QueryRowxContext(ctx, addCompany, company.Token, company.OwnerId, company.Name, company.Email).StructScan(&newCompany) + err := s.db.QueryRowxContext(ctx, addCompany, company.Token, company.OwnerID, company.Name, company.Email).StructScan(&newCompany) if err != nil { return newCompany, fmt.Errorf("%s: execute query: %w", op, err) } @@ -40,6 +43,8 @@ func (s *CompanyStorage) AddCompany(ctx context.Context, company entities.Compan return newCompany, nil } +// GetCompanyByOwnerTelegramId retrieves a company by the owner's Telegram ID. +// If the company is not found, ErrNotFound is returned. func (s *CompanyStorage) GetCompanyByOwnerTelegramId(ctx context.Context, ownerId int64) (entities.Company, error) { const op = "storage.postgres.GetCompanyByOwnerId" diff --git a/internal/storage/postgres/company/company_test.go b/internal/storage/postgres/company/company_test.go index 8f636c4..36ddfb7 100644 --- a/internal/storage/postgres/company/company_test.go +++ b/internal/storage/postgres/company/company_test.go @@ -21,9 +21,9 @@ func TestCompanyStorage_AddCompany(t *testing.T) { f := database.NewFixture(t) defer f.Teardown() expectedCompany := entities.Company{ - Id: 12, + ID: 12, Token: "bguFFFTF&ffdR9*9u", - OwnerId: 21, + OwnerID: 21, Name: "MyCompany", Email: "info@google.com", } @@ -31,7 +31,7 @@ func TestCompanyStorage_AddCompany(t *testing.T) { AddRow(12, "bguFFFTF&ffdR9*9u", 21, "MyCompany", "info@google.com") f.Mock.ExpectQuery(regexp.QuoteMeta("INSERT INTO companies (token, owner_id, name, email) VALUES ($1, $2, $3, $4) RETURNING id, token, owner_id, name, email")). - WithArgs(expectedCompany.Token, expectedCompany.OwnerId, expectedCompany.Name, expectedCompany.Email). + WithArgs(expectedCompany.Token, expectedCompany.OwnerID, expectedCompany.Name, expectedCompany.Email). WillReturnRows(rows) repo := New(f.DB) @@ -52,15 +52,15 @@ func TestCompanyStorage_AddCompany(t *testing.T) { expectErr := errors.New("test error") expectedCompany := entities.Company{ - Id: 12, + ID: 12, Token: "bguFFFTF&ffdR9*9u", - OwnerId: 21, + OwnerID: 21, Name: "MyCompany", Email: "info@google.com", } f.Mock.ExpectQuery(regexp.QuoteMeta("INSERT INTO companies (token, owner_id, name, email) VALUES ($1, $2, $3, $4) RETURNING id, token, owner_id, name, email")). - WithArgs(expectedCompany.Token, expectedCompany.OwnerId, expectedCompany.Name, expectedCompany.Email). + WithArgs(expectedCompany.Token, expectedCompany.OwnerID, expectedCompany.Name, expectedCompany.Email). WillReturnError(expectErr) repo := New(f.DB) @@ -83,8 +83,8 @@ func TestCompanyStorage_GetCompanyByOwnerTelegramId(t *testing.T) { var id int64 = 21 companyExp := entities.Company{ - Id: 12, - OwnerId: 13, + ID: 12, + OwnerID: 13, Token: "bguFFFTF&ffdR9*9u", Name: "MyCompany", Email: "info@ya.ru", diff --git a/internal/storage/postgres/owner/owner.go b/internal/storage/postgres/owner/owner.go index ff3307c..26c9ffc 100644 --- a/internal/storage/postgres/owner/owner.go +++ b/internal/storage/postgres/owner/owner.go @@ -11,10 +11,12 @@ import ( "github.com/testit-tms/webhook-bot/internal/storage" ) +// OwnerStorage represents a PostgreSQL implementation of the storage for owners. type OwnerStorage struct { db *sqlx.DB } +// New creates a new instance of OwnerStorage with the given database connection. func New(db *sqlx.DB) *OwnerStorage { return &OwnerStorage{ db: db, @@ -28,6 +30,7 @@ const ( deleteOwnerById = "DELETE FROM owners WHERE id=$1" ) +// GetOwnerById retrieves an owner by their ID. func (s *OwnerStorage) GetOwnerById(ctx context.Context, id int64) (entities.Owner, error) { const op = "storage.postgres.GetOwnerById" @@ -44,6 +47,7 @@ func (s *OwnerStorage) GetOwnerById(ctx context.Context, id int64) (entities.Own return owner, nil } +// GetOwnerByTelegramId retrieves an owner by their Telegram ID. func (s *OwnerStorage) GetOwnerByTelegramId(ctx context.Context, id int64) (entities.Owner, error) { const op = "storage.postgres.GetOwnerByTelegramId" @@ -60,12 +64,13 @@ func (s *OwnerStorage) GetOwnerByTelegramId(ctx context.Context, id int64) (enti return owner, nil } +// AddOwner adds a new owner to the database and returns the newly created owner. func (r *OwnerStorage) AddOwner(ctx context.Context, owner entities.Owner) (entities.Owner, error) { const op = "storage.postgres.AddOwner" newOwner := entities.Owner{} - err := r.db.QueryRowxContext(ctx, addOwner, owner.TelegramId, owner.TelegramName).StructScan(&newOwner) + err := r.db.QueryRowxContext(ctx, addOwner, owner.TelegramID, owner.TelegramName).StructScan(&newOwner) if err != nil { return newOwner, fmt.Errorf("%s: execute query: %w", op, err) } @@ -73,7 +78,8 @@ func (r *OwnerStorage) AddOwner(ctx context.Context, owner entities.Owner) (enti return newOwner, nil } -func (r *OwnerStorage) DeleteOwnerById(ctx context.Context, id int64) error { +// DeleteOwnerByID deletes an owner by their ID. +func (r *OwnerStorage) DeleteOwnerByID(ctx context.Context, id int64) error { const op = "storage.postgres.DeleteOwnerById" _, err := r.db.ExecContext(ctx, deleteOwnerById, id) diff --git a/internal/storage/postgres/owner/owner_test.go b/internal/storage/postgres/owner/owner_test.go index 1c40697..3436a48 100644 --- a/internal/storage/postgres/owner/owner_test.go +++ b/internal/storage/postgres/owner/owner_test.go @@ -23,8 +23,8 @@ func TestOwnerStorage_GetOwnerById(t *testing.T) { var id int64 = 12 ownerExp := entities.Owner{ - Id: id, - TelegramId: 123456, + ID: id, + TelegramID: 123456, TelegramName: "Mega Owner", } @@ -96,8 +96,8 @@ func TestOwnerStorage_GetOwnerByTelegramId(t *testing.T) { var id int64 = 123456 ownerExp := entities.Owner{ - Id: 12, - TelegramId: id, + ID: 12, + TelegramID: id, TelegramName: "Mega Owner", } @@ -167,15 +167,15 @@ func TestOwnerStorage_AddOwner(t *testing.T) { f := database.NewFixture(t) defer f.Teardown() expectedOwner := entities.Owner{ - Id: 12, - TelegramId: 123456, + ID: 12, + TelegramID: 123456, TelegramName: "MyName", } rows := sqlmock.NewRows([]string{"id", "telegram_id", "telegram_name"}). AddRow(12, "123456", "MyName") f.Mock.ExpectQuery(regexp.QuoteMeta("INSERT INTO owners (telegram_id, telegram_name) VALUES ($1, $2) RETURNING id, telegram_id, telegram_name")). - WithArgs(expectedOwner.TelegramId, expectedOwner.TelegramName). + WithArgs(expectedOwner.TelegramID, expectedOwner.TelegramName). WillReturnRows(rows) repo := New(f.DB) @@ -196,13 +196,13 @@ func TestOwnerStorage_AddOwner(t *testing.T) { expectErr := errors.New("test error") expectedOwner := entities.Owner{ - Id: 12, - TelegramId: 123456, + ID: 12, + TelegramID: 123456, TelegramName: "MyName", } f.Mock.ExpectQuery(regexp.QuoteMeta("INSERT INTO owners (telegram_id, telegram_name) VALUES ($1, $2) RETURNING id, telegram_id, telegram_name")). - WithArgs(expectedOwner.TelegramId, expectedOwner.TelegramName). + WithArgs(expectedOwner.TelegramID, expectedOwner.TelegramName). WillReturnError(expectErr) repo := New(f.DB) @@ -222,7 +222,7 @@ func TestOwnerStorage_DeleteOwnerById(t *testing.T) { t.Parallel() f := database.NewFixture(t) defer f.Teardown() - + var id int64 = 12 f.Mock.ExpectExec(regexp.QuoteMeta("DELETE FROM owners WHERE id=$1")). @@ -232,7 +232,7 @@ func TestOwnerStorage_DeleteOwnerById(t *testing.T) { repo := New(f.DB) // Act - err := repo.DeleteOwnerById(context.Background(), id) + err := repo.DeleteOwnerByID(context.Background(), id) // Assert assert.NoError(t, err) @@ -254,7 +254,7 @@ func TestOwnerStorage_DeleteOwnerById(t *testing.T) { repo := New(f.DB) // Act - err := repo.DeleteOwnerById(context.Background(), id) + err := repo.DeleteOwnerByID(context.Background(), id) // Assert assert.ErrorIs(t, err, expectErr) diff --git a/internal/storage/storage.go b/internal/storage/storage.go index db7a931..da1f7b2 100644 --- a/internal/storage/storage.go +++ b/internal/storage/storage.go @@ -3,5 +3,6 @@ package storage import "errors" var ( + // ErrNotFound is returned when an entity is not found. ErrNotFound = errors.New("entity not found") -) \ No newline at end of file +) diff --git a/internal/transport/rest/send/model.go b/internal/transport/rest/send/model.go index 86b3e46..11c86a7 100644 --- a/internal/transport/rest/send/model.go +++ b/internal/transport/rest/send/model.go @@ -2,6 +2,7 @@ package send import "github.com/testit-tms/webhook-bot/internal/entities" +// Request represents a request to send a message. type Request struct { Message string `json:"message" validate:"required"` ParseMode string `json:"parseMode,omitempty" validate:"parse-mode"` diff --git a/internal/transport/rest/send/send.go b/internal/transport/rest/send/send.go index 883cbae..f60ab49 100644 --- a/internal/transport/rest/send/send.go +++ b/internal/transport/rest/send/send.go @@ -19,6 +19,10 @@ type sender interface { SendMessage(ctx context.Context, msg entities.Message) error } +// New returns a new http.HandlerFunc that sends a message using the provided sender. +// It validates the request, converts it to a message, and sends it using the sender. +// If any error occurs during the process, it returns an error response. +// It requires an Authorization token in the request header. func New(log *slog.Logger, sender sender) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { const op = "transport.rest.send.New" diff --git a/internal/transport/rest/send/send_test.go b/internal/transport/rest/send/send_test.go index cca35f1..03f9532 100644 --- a/internal/transport/rest/send/send_test.go +++ b/internal/transport/rest/send/send_test.go @@ -107,7 +107,6 @@ func TestNew(t *testing.T) { mockTimes: 1, mockError: errors.New("some error"), }, - } for _, tc := range tests { tc := tc diff --git a/internal/transport/telegram/commands/chat.go b/internal/transport/telegram/commands/chat.go index 6a0674f..01ea218 100644 --- a/internal/transport/telegram/commands/chat.go +++ b/internal/transport/telegram/commands/chat.go @@ -19,6 +19,7 @@ type chatCommands struct { compu companyUsesaces } +// NewChatCommands returns a new instance of chatCommands with the provided chatUsecases and companyUsesaces. func NewChatCommands(cu chatUsecases, compu companyUsesaces) *chatCommands { return &chatCommands{ cu: cu, @@ -26,6 +27,12 @@ func NewChatCommands(cu chatUsecases, compu companyUsesaces) *chatCommands { } } +// AddChat adds a new chat to the company with the owner's Telegram ID. +// It takes a Telegram message as input and extracts the chat ID from the command arguments. +// If the chat ID is not a valid integer, it returns an error. +// It then retrieves the company associated with the owner's Telegram ID and adds the chat to the company. +// If there is an error while adding the chat, it returns an error. +// Otherwise, it returns a success message. func (c *chatCommands) AddChat(m *tgbotapi.Message) (tgbotapi.MessageConfig, error) { const op = "chatCommands.AddChat" @@ -43,8 +50,8 @@ func (c *chatCommands) AddChat(m *tgbotapi.Message) (tgbotapi.MessageConfig, err } _, err = c.cu.AddChat(context.Background(), entities.Chat{ - CompanyId: company.Id, - TelegramId: int64(chatID), + CompanyID: company.ID, + TelegramID: int64(chatID), }) if err != nil { return tgbotapi.NewMessage(m.Chat.ID, "Something went wrong. Lets try again"), @@ -54,6 +61,11 @@ func (c *chatCommands) AddChat(m *tgbotapi.Message) (tgbotapi.MessageConfig, err return tgbotapi.NewMessage(m.Chat.ID, "Chat added"), nil } +// DeleteChat deletes a chat by its ID. It takes a Telegram message as input and extracts the chat ID from the command arguments. +// It then calls the DeleteChatByTelegramId method of the ChatUseCase to delete the chat from the database. +// If the chat ID is not a valid integer, it returns an error and a message to the user. +// If there is an error deleting the chat, it returns an error and a message to the user. +// Otherwise, it returns a success message to the user. func (c *chatCommands) DeleteChat(m *tgbotapi.Message) (tgbotapi.MessageConfig, error) { const op = "chatCommands.DeleteChat" diff --git a/internal/transport/telegram/commands/company.go b/internal/transport/telegram/commands/company.go index 80e30fd..4399b8b 100644 --- a/internal/transport/telegram/commands/company.go +++ b/internal/transport/telegram/commands/company.go @@ -14,10 +14,12 @@ type companyUsesaces interface { GetCompanyByOwnerTelegramId(ctx context.Context, ownerId int64) (entities.CompanyInfo, error) } +// CompanyCommands represents a set of commands related to companies. type CompanyCommands struct { cu companyUsesaces } +// NewCompanyCommands creates a new instance of CompanyCommands with the provided company use cases. func NewCompanyCommands(cu companyUsesaces) *CompanyCommands { return &CompanyCommands{ cu: cu, @@ -25,6 +27,10 @@ func NewCompanyCommands(cu companyUsesaces) *CompanyCommands { } // TODO: rename to GetMyCompany and refactor + +// GetMyCompanies returns a Telegram message containing information about the company owned by the user who sent the message. +// If the user does not own any companies, the message will indicate that they have no companies and provide a command to register a new one. +// If an error occurs while retrieving the company information, an error message will be returned. func (c *CompanyCommands) GetMyCompanies(m *tgbotapi.Message) (tgbotapi.MessageConfig, error) { const op = "CompanyCommands.GetMyCompanies" diff --git a/internal/transport/telegram/commands/help.go b/internal/transport/telegram/commands/help.go index cbaecd9..431c18e 100644 --- a/internal/transport/telegram/commands/help.go +++ b/internal/transport/telegram/commands/help.go @@ -4,6 +4,9 @@ import ( tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5" ) +// GetHelpMessage returns a Telegram message configuration containing a list of available commands. +// The message includes the command names and their descriptions. +// The message is sent to the chat ID specified in the input message. func GetHelpMessage(m *tgbotapi.Message) tgbotapi.MessageConfig { return tgbotapi.NewMessage(m.Chat.ID, `Available commands: diff --git a/internal/transport/telegram/commands/register.go b/internal/transport/telegram/commands/register.go index 564823c..ae7e80c 100644 --- a/internal/transport/telegram/commands/register.go +++ b/internal/transport/telegram/commands/register.go @@ -20,12 +20,16 @@ type regUsecase interface { } // TODO: move to companyCommands + +// Registrator represents a struct that is responsible for handling the registration process for new companies. type Registrator struct { logger *slog.Logger waitMap map[int64]models.Company u regUsecase } +// NewRegistrator creates a new instance of the Registrator struct, which is responsible for handling the registration process for new companies. +// It takes a logger instance and a regUsecase instance as arguments, and returns a pointer to the new Registrator instance. func NewRegistrator(logger *slog.Logger, u regUsecase) *Registrator { return &Registrator{ logger: logger, @@ -54,6 +58,8 @@ func (r *Registrator) registerEmail(chatID int64, email string) { } } +// Action performs the registration process for a user and returns the next step to be taken. +// It takes a message and a step number as input and returns a message configuration and the next step number. func (r *Registrator) Action(m *tgbotapi.Message, step int) (tgbotapi.MessageConfig, int) { const op = "Registrator.Action" logger := r.logger.With( @@ -97,6 +103,7 @@ func (r *Registrator) Action(m *tgbotapi.Message, step int) (tgbotapi.MessageCon } } +// GetFirstMessage returns the first message to be sent to a user during the registration process. func (r *Registrator) GetFirstMessage(m *tgbotapi.Message) tgbotapi.MessageConfig { e, err := r.u.CheckCompanyExists(context.Background(), m.From.ID) if err != nil { @@ -107,7 +114,7 @@ func (r *Registrator) GetFirstMessage(m *tgbotapi.Message) tgbotapi.MessageConfi if e { return tgbotapi.NewMessage(m.Chat.ID, "You already have company") } - + msg := tgbotapi.NewMessage(m.Chat.ID, "Enter company name:") msg.ReplyMarkup = tgbotapi.ForceReply{ForceReply: true, Selective: true} return msg diff --git a/internal/transport/telegram/commands/show.go b/internal/transport/telegram/commands/show.go index 0567efd..eb39756 100644 --- a/internal/transport/telegram/commands/show.go +++ b/internal/transport/telegram/commands/show.go @@ -6,6 +6,8 @@ import ( tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5" ) +// GetChatId returns a MessageConfig that contains the chat ID of the given message. +// The chat ID is included in the message text. func GetChatId(m *tgbotapi.Message) tgbotapi.MessageConfig { return tgbotapi.NewMessage(m.Chat.ID, fmt.Sprintf("Chat ID: %d", m.Chat.ID)) } diff --git a/internal/transport/telegram/commands/start.go b/internal/transport/telegram/commands/start.go index f9b13ae..91cfe19 100644 --- a/internal/transport/telegram/commands/start.go +++ b/internal/transport/telegram/commands/start.go @@ -4,6 +4,10 @@ import ( tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5" ) +// GetStartMessage returns a Telegram message configuration for the start command response. +// The message contains a welcome message and instructions on how to use the Test IT webhook bot. +// Parameter m is a pointer to a tgbotapi.Message struct representing the incoming message. +// The returned value is a tgbotapi.MessageConfig struct representing the message to be sent. func GetStartMessage(m *tgbotapi.Message) tgbotapi.MessageConfig { return tgbotapi.NewMessage(m.Chat.ID, `Hi! Welcome to the Test IT webhook bot! @@ -13,4 +17,4 @@ func GetStartMessage(m *tgbotapi.Message) tgbotapi.MessageConfig { Also you can see the list of available commands using the /help command. `) -} \ No newline at end of file +} diff --git a/internal/transport/telegram/conversation.go b/internal/transport/telegram/conversation.go index e5b97b1..32cf4c9 100644 --- a/internal/transport/telegram/conversation.go +++ b/internal/transport/telegram/conversation.go @@ -1,5 +1,6 @@ package telegram +// Conversation represents a conversation with a user. type Conversation struct { typeOfConversation typeOfConversation step int diff --git a/internal/transport/telegram/models/company.go b/internal/transport/telegram/models/company.go index 28dd3f9..c936516 100644 --- a/internal/transport/telegram/models/company.go +++ b/internal/transport/telegram/models/company.go @@ -2,18 +2,21 @@ package models import "github.com/testit-tms/webhook-bot/internal/entities" +// Company represents a company that is registering for the webhook bot service. type Company struct { User User CompanyName string `validate:"min=1,max=100"` Email string `validate:"email"` } +// ToCompanyInfo converts a Company struct to a CompanyRegistrationInfo struct. +// It returns a new CompanyRegistrationInfo struct with the converted data. func (c Company) ToCompanyInfo() entities.CompanyRegistrationInfo { return entities.CompanyRegistrationInfo{ Name: c.CompanyName, Email: c.Email, Owner: entities.OwnerInfo{ - TelegramId: c.User.ID, + TelegramID: c.User.ID, TelegramName: c.User.Name, }, } diff --git a/internal/transport/telegram/models/user.go b/internal/transport/telegram/models/user.go index 7aaff05..4b3c8ce 100644 --- a/internal/transport/telegram/models/user.go +++ b/internal/transport/telegram/models/user.go @@ -1,5 +1,6 @@ package models +// User represents a Telegram user. type User struct { ID int64 Name string diff --git a/internal/transport/telegram/telegram.go b/internal/transport/telegram/telegram.go index d48c1d4..ecb5e98 100644 --- a/internal/transport/telegram/telegram.go +++ b/internal/transport/telegram/telegram.go @@ -36,6 +36,7 @@ type chatCommands interface { DeleteChat(m *tgbotapi.Message) (tgbotapi.MessageConfig, error) } +// TelegramBot represents a Telegram bot instance type TelegramBot struct { logger *slog.Logger bot *tgbotapi.BotAPI @@ -155,6 +156,7 @@ func (b *TelegramBot) sendMessage(m tgbotapi.MessageConfig) { } } +// SendMessage sends a message to the specified chat IDs using the Telegram bot API. func (b *TelegramBot) SendMessage(ctx context.Context, msg entities.Message) error { const op = "telegram.SendMessage" for _, chatID := range msg.ChatIds { diff --git a/internal/usecases/chat.go b/internal/usecases/chat.go index 3bf69e9..f464305 100644 --- a/internal/usecases/chat.go +++ b/internal/usecases/chat.go @@ -46,13 +46,13 @@ func (u *chatUsecases) DeleteChatByTelegramId(ctx context.Context, ownerId, chat return fmt.Errorf("%s: get company by owner id: %w", op, err) } - chats, err := u.cs.GetChatsByCompanyId(ctx, company.Id) + chats, err := u.cs.GetChatsByCompanyId(ctx, company.ID) if err != nil { return fmt.Errorf("%s: get chats by company id: %w", op, err) } for _, chat := range chats { - if chat.TelegramId == chatId { + if chat.TelegramID == chatId { if err := u.cs.DeleteChatById(ctx, chat.Id); err != nil { return fmt.Errorf("%s: delete chat by id: %w", op, err) } diff --git a/internal/usecases/chat_test.go b/internal/usecases/chat_test.go index 9c9570a..f45a5e6 100644 --- a/internal/usecases/chat_test.go +++ b/internal/usecases/chat_test.go @@ -25,13 +25,13 @@ func Test_chatUsecases_AddChat(t *testing.T) { { name: "success", chat: entities.Chat{ - CompanyId: 12, - TelegramId: 123, + CompanyID: 12, + TelegramID: 123, }, want: entities.Chat{ Id: 1, - CompanyId: 12, - TelegramId: 123, + CompanyID: 12, + TelegramID: 123, }, wantErr: false, wantErrMessage: "", @@ -40,8 +40,8 @@ func Test_chatUsecases_AddChat(t *testing.T) { { name: "error", chat: entities.Chat{ - CompanyId: 12, - TelegramId: 123, + CompanyID: 12, + TelegramID: 123, }, want: entities.Chat{}, wantErr: true, @@ -96,15 +96,15 @@ func Test_chatUsecases_DeleteChatByTelegramId(t *testing.T) { ownerId: 1, chatId: 123, mockCompEntities: entities.Company{ - Id: 12, + ID: 12, }, mockCompError: nil, mockCompTimes: 1, mockGetChatEntities: []entities.Chat{ { Id: 1, - CompanyId: 12, - TelegramId: 123, + CompanyID: 12, + TelegramID: 123, }, }, mockGetChatError: nil, @@ -129,15 +129,15 @@ func Test_chatUsecases_DeleteChatByTelegramId(t *testing.T) { ownerId: 1, chatId: 123, mockCompEntities: entities.Company{ - Id: 12, + ID: 12, }, mockCompError: nil, mockCompTimes: 1, mockGetChatEntities: []entities.Chat{ { Id: 1, - CompanyId: 12, - TelegramId: 321, + CompanyID: 12, + TelegramID: 321, }, }, mockGetChatError: nil, @@ -152,7 +152,7 @@ func Test_chatUsecases_DeleteChatByTelegramId(t *testing.T) { ownerId: 1, chatId: 123, mockCompEntities: entities.Company{ - Id: 12, + ID: 12, }, mockCompError: nil, mockCompTimes: 1, @@ -165,19 +165,19 @@ func Test_chatUsecases_DeleteChatByTelegramId(t *testing.T) { wantErrMessage: "usecases.DeleteChatByTelegramId: get chats by company id: error", }, { - name: "delete chat error", + name: "delete chat error", ownerId: 1, chatId: 123, mockCompEntities: entities.Company{ - Id: 12, + ID: 12, }, mockCompError: nil, mockCompTimes: 1, mockGetChatEntities: []entities.Chat{ { Id: 1, - CompanyId: 12, - TelegramId: 123, + CompanyID: 12, + TelegramID: 123, }, }, mockGetChatError: nil, @@ -187,8 +187,6 @@ func Test_chatUsecases_DeleteChatByTelegramId(t *testing.T) { wantErr: true, wantErrMessage: "usecases.DeleteChatByTelegramId: delete chat by id: error", }, - - } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -199,7 +197,7 @@ func Test_chatUsecases_DeleteChatByTelegramId(t *testing.T) { chatMock := mocks.NewMockchatsStorage(mockCtrl) if tt.mockGetChatTimes != 0 { - chatMock.EXPECT().GetChatsByCompanyId(gomock.Any(), tt.mockCompEntities.Id).Return(tt.mockGetChatEntities, tt.mockGetChatError).Times(tt.mockGetChatTimes) + chatMock.EXPECT().GetChatsByCompanyId(gomock.Any(), tt.mockCompEntities.ID).Return(tt.mockGetChatEntities, tt.mockGetChatError).Times(tt.mockGetChatTimes) } if tt.mockDeleteChatTimes != 0 { diff --git a/internal/usecases/company.go b/internal/usecases/company.go index e93a315..8d10f88 100644 --- a/internal/usecases/company.go +++ b/internal/usecases/company.go @@ -25,9 +25,11 @@ type companyUsecases struct { } var ( + // ErrCompanyNotFound is returned when a company is not found. ErrCompanyNotFound = errors.New("company not found") ) +// NewCompanyUsecases creates a new instance of companyUsecases. func NewCompanyUsecases(cs companyStorage, chs chatStorage) *companyUsecases { return &companyUsecases{ cs: cs, @@ -35,6 +37,8 @@ func NewCompanyUsecases(cs companyStorage, chs chatStorage) *companyUsecases { } } +// GetCompanyByOwnerTelegramId retrieves the company information associated with the given owner Telegram ID. +// It returns a CompanyInfo struct and an error. If the company is not found, it returns ErrCompanyNotFound. func (u *companyUsecases) GetCompanyByOwnerTelegramId(ctx context.Context, ownerId int64) (entities.CompanyInfo, error) { const op = "usecases.GetCompanyByOwnerTelegramId" @@ -47,14 +51,14 @@ func (u *companyUsecases) GetCompanyByOwnerTelegramId(ctx context.Context, owner } ci := entities.CompanyInfo{ - Id: company.Id, - OwnerId: company.OwnerId, + ID: company.ID, + OwnerID: company.OwnerID, Token: company.Token, Name: company.Name, Email: company.Email, } - chats, err := u.chs.GetChatsByCompanyId(ctx, company.Id) + chats, err := u.chs.GetChatsByCompanyId(ctx, company.ID) if err != nil { if errors.Is(err, storage.ErrNotFound) { return ci, nil @@ -63,7 +67,7 @@ func (u *companyUsecases) GetCompanyByOwnerTelegramId(ctx context.Context, owner } for _, chat := range chats { - ci.ChatIds = append(ci.ChatIds, chat.TelegramId) + ci.ChatIds = append(ci.ChatIds, chat.TelegramID) } return ci, nil diff --git a/internal/usecases/company_test.go b/internal/usecases/company_test.go index c843e14..7b2eb04 100644 --- a/internal/usecases/company_test.go +++ b/internal/usecases/company_test.go @@ -31,8 +31,8 @@ func Test_companyUsecases_GetCompanyByOwnerId(t *testing.T) { name: "success", ownerId: 1, want: entities.CompanyInfo{ - Id: 12, - OwnerId: 21, + ID: 12, + OwnerID: 21, Token: "token", Name: "Yandex", Email: "info@ya.ru", @@ -41,8 +41,8 @@ func Test_companyUsecases_GetCompanyByOwnerId(t *testing.T) { }, }, mockCompEntities: entities.Company{ - Id: 12, - OwnerId: 21, + ID: 12, + OwnerID: 21, Token: "token", Name: "Yandex", Email: "info@ya.ru", @@ -52,8 +52,8 @@ func Test_companyUsecases_GetCompanyByOwnerId(t *testing.T) { mockChatEntities: []entities.Chat{ { Id: 1, - CompanyId: 12, - TelegramId: 123, + CompanyID: 12, + TelegramID: 123, }, }, mockChatError: nil, @@ -86,16 +86,16 @@ func Test_companyUsecases_GetCompanyByOwnerId(t *testing.T) { name: "chats with ErrNotFound", ownerId: 1, want: entities.CompanyInfo{ - Id: 12, - OwnerId: 21, + ID: 12, + OwnerID: 21, Token: "token", Name: "Yandex", Email: "info@ya.ru", ChatIds: nil, }, mockCompEntities: entities.Company{ - Id: 12, - OwnerId: 21, + ID: 12, + OwnerID: 21, Token: "token", Name: "Yandex", Email: "info@ya.ru", @@ -111,16 +111,16 @@ func Test_companyUsecases_GetCompanyByOwnerId(t *testing.T) { name: "chats with other error", ownerId: 1, want: entities.CompanyInfo{ - Id: 12, - OwnerId: 21, + ID: 12, + OwnerID: 21, Token: "token", Name: "Yandex", Email: "info@ya.ru", ChatIds: nil, }, mockCompEntities: entities.Company{ - Id: 12, - OwnerId: 21, + ID: 12, + OwnerID: 21, Token: "token", Name: "Yandex", Email: "info@ya.ru", @@ -142,7 +142,7 @@ func Test_companyUsecases_GetCompanyByOwnerId(t *testing.T) { chatMock := mocks.NewMockchatStorage(mockCtrl) if tt.mockChatTimes != 0 { - chatMock.EXPECT().GetChatsByCompanyId(gomock.Any(), tt.mockCompEntities.Id).Return(tt.mockChatEntities, tt.mockChatError).Times(tt.mockChatTimes) + chatMock.EXPECT().GetChatsByCompanyId(gomock.Any(), tt.mockCompEntities.ID).Return(tt.mockChatEntities, tt.mockChatError).Times(tt.mockChatTimes) } u := NewCompanyUsecases(companyMock, chatMock) diff --git a/internal/usecases/registration/registration.go b/internal/usecases/registration/registration.go index 566a71c..350bcdc 100644 --- a/internal/usecases/registration/registration.go +++ b/internal/usecases/registration/registration.go @@ -21,15 +21,19 @@ type companyStorage interface { } var ( + // ErrCompanyAlreadyExists is returned when a company already exists. ErrCompanyAlreadyExists = fmt.Errorf("company already exists") ) // TODO: move to usesases package and write tests + +// RegistrationUsecases is a usecase implementation for registration. type RegistrationUsecases struct { os ownerStorage cs companyStorage } +// New creates a new instance of RegistrationUsecases. func New(os ownerStorage, cs companyStorage) *RegistrationUsecases { return &RegistrationUsecases{ os: os, @@ -38,6 +42,8 @@ func New(os ownerStorage, cs companyStorage) *RegistrationUsecases { } // TODO: move to usesases package and write tests + +// CheckCompanyExists checks if a company exists for the given owner ID. func (r *RegistrationUsecases) CheckCompanyExists(ctx context.Context, ownerId int64) (bool, error) { const op = "RegistrationUsecases.CheckCompanyExists" @@ -54,17 +60,19 @@ func (r *RegistrationUsecases) CheckCompanyExists(ctx context.Context, ownerId i } // TODO: add transaction and tests + +// RegisterCompany registers a new company with the given registration information. func (r *RegistrationUsecases) RegisterCompany(ctx context.Context, c entities.CompanyRegistrationInfo) error { const op = "RegistrationUsecases.RegisterCompany" - owner, err := r.os.GetOwnerByTelegramId(ctx, c.Owner.TelegramId) + owner, err := r.os.GetOwnerByTelegramId(ctx, c.Owner.TelegramID) if err != nil { if err != storage.ErrNotFound { return fmt.Errorf("%s: cannot get owner by telegram id: %w", op, err) } newOwner := entities.Owner{ - TelegramId: c.Owner.TelegramId, + TelegramID: c.Owner.TelegramID, TelegramName: c.Owner.TelegramName, } @@ -74,13 +82,13 @@ func (r *RegistrationUsecases) RegisterCompany(ctx context.Context, c entities.C } } - _, err = r.cs.GetCompanyByOwnerTelegramId(ctx, c.Owner.TelegramId) + _, err = r.cs.GetCompanyByOwnerTelegramId(ctx, c.Owner.TelegramID) if err != nil { if errors.Is(err, storage.ErrNotFound) { company := entities.Company{ Name: c.Name, Email: c.Email, - OwnerId: owner.Id, + OwnerID: owner.ID, Token: random.NewRandomString(30), } diff --git a/internal/usecases/sendmessage.go b/internal/usecases/sendmessage.go index 68580e4..e424659 100644 --- a/internal/usecases/sendmessage.go +++ b/internal/usecases/sendmessage.go @@ -27,11 +27,15 @@ type sendMessageUsacases struct { } var ( + // ErrChatsNotFound is returned when chats are not found. ErrChatsNotFound = errors.New("chats not found") + // ErrChatsNotAllow is returned when chats are not allowed. ErrChatsNotAllow = errors.New("chats not allowed") + // ErrCanNotSend is returned when a message cannot be sent. ErrCanNotSend = errors.New("can not send message") ) +// NewSendMessageUsecases creates a new instance of sendMessageUsacases with the provided dependencies. func NewSendMessageUsecases(logger *slog.Logger, cg chatGeter, bs botSender) *sendMessageUsacases { return &sendMessageUsacases{ logger: logger, @@ -40,6 +44,9 @@ func NewSendMessageUsecases(logger *slog.Logger, cg chatGeter, bs botSender) *se } } +// SendMessage sends a message to the specified chats. If no chat IDs are provided, the message is sent to all chats associated with the company token. +// If chat IDs are provided, the message is only sent to the chats that are associated with the company token and have a matching chat ID. +// Returns an error if the chats are not found, not allowed, or if the message cannot be sent. func (u *sendMessageUsacases) SendMessage(ctx context.Context, msg entities.Message) error { const op = "usecases.SendMessage" logger := u.logger.With(slog.String("operation", op)) @@ -56,7 +63,7 @@ func (u *sendMessageUsacases) SendMessage(ctx context.Context, msg entities.Mess if len(msg.ChatIds) == 0 { for _, c := range chats { - msg.ChatIds = append(msg.ChatIds, c.TelegramId) + msg.ChatIds = append(msg.ChatIds, c.TelegramID) } err := u.bs.SendMessage(ctx, msg) if err != nil { @@ -70,8 +77,8 @@ func (u *sendMessageUsacases) SendMessage(ctx context.Context, msg entities.Mess allowedChats := make([]int64, 0, len(msg.ChatIds)) for _, chat := range msg.ChatIds { for _, c := range chats { - if c.TelegramId == chat { - allowedChats = append(allowedChats, c.TelegramId) + if c.TelegramID == chat { + allowedChats = append(allowedChats, c.TelegramID) } } } diff --git a/internal/usecases/sendmessage_test.go b/internal/usecases/sendmessage_test.go index 37b891b..509d7be 100644 --- a/internal/usecases/sendmessage_test.go +++ b/internal/usecases/sendmessage_test.go @@ -37,8 +37,8 @@ func Test_sendMessageUsacases_SendMessage(t *testing.T) { mockChatEntities: []entities.Chat{ { Id: 1, - TelegramId: 123, - CompanyId: 12, + TelegramID: 123, + CompanyID: 12, }, }, mockChatError: nil, @@ -108,8 +108,8 @@ func Test_sendMessageUsacases_SendMessage(t *testing.T) { mockChatEntities: []entities.Chat{ { Id: 1, - TelegramId: 123, - CompanyId: 12, + TelegramID: 123, + CompanyID: 12, }, }, mockChatError: nil, @@ -135,8 +135,8 @@ func Test_sendMessageUsacases_SendMessage(t *testing.T) { mockChatEntities: []entities.Chat{ { Id: 1, - TelegramId: 123, - CompanyId: 12, + TelegramID: 123, + CompanyID: 12, }, }, mockChatError: nil, @@ -163,8 +163,8 @@ func Test_sendMessageUsacases_SendMessage(t *testing.T) { mockChatEntities: []entities.Chat{ { Id: 1, - TelegramId: 123, - CompanyId: 12, + TelegramID: 123, + CompanyID: 12, }, }, mockChatError: nil, @@ -191,8 +191,8 @@ func Test_sendMessageUsacases_SendMessage(t *testing.T) { mockChatEntities: []entities.Chat{ { Id: 1, - TelegramId: 123, - CompanyId: 12, + TelegramID: 123, + CompanyID: 12, }, }, mockChatError: nil, diff --git a/pkg/database/fixture.go b/pkg/database/fixture.go index 8ff7086..af2218f 100644 --- a/pkg/database/fixture.go +++ b/pkg/database/fixture.go @@ -8,12 +8,16 @@ import ( "github.com/jmoiron/sqlx" ) +// Fixture represents a test fixture for database testing. type Fixture struct { Mock sqlmock.Sqlmock DB *sqlx.DB con *sql.DB } +// NewFixture returns a new instance of Fixture for testing purposes. +// It creates a new sqlmock database connection and returns a Fixture instance +// with the sqlmock DB and connection. func NewFixture(t *testing.T) *Fixture { mockDB, mock, _ := sqlmock.New() @@ -22,6 +26,7 @@ func NewFixture(t *testing.T) *Fixture { return &Fixture{Mock: mock, DB: db, con: mockDB} } +// Teardown closes the connection to the mock database. func (f *Fixture) Teardown() { f.con.Close() } diff --git a/pkg/database/postgres.go b/pkg/database/postgres.go index 1921456..0812113 100644 --- a/pkg/database/postgres.go +++ b/pkg/database/postgres.go @@ -16,6 +16,7 @@ const ( driverName = "postgres" ) +// Initialize a new connection to a PostgreSQL database and runs any pending migrations. func Initialize(host string, port int64, user, password, dbname string) (*sqlx.DB, error) { const op = "database.Initialize" db, err := sqlx.Connect(driverName, fmt.Sprintf("host=%s port=%d user=%s password=%s dbname=%s sslmode=disable", diff --git a/pkg/logger/logger.go b/pkg/logger/logger.go index c0bf763..b29464d 100644 --- a/pkg/logger/logger.go +++ b/pkg/logger/logger.go @@ -8,6 +8,9 @@ import ( "golang.org/x/exp/slog" ) +// New creates a new logger instance with the specified log level. +// The log level should be one of "debug", "info", "warn", or "error". +// Returns a pointer to the logger instance and an error if the log level is invalid. func New(level string) (*slog.Logger, error) { logLevel, err := getLogLevel(level) if err != nil {