diff --git a/internal/entities/alert.go b/internal/entities/alert.go new file mode 100644 index 0000000..0d62e53 --- /dev/null +++ b/internal/entities/alert.go @@ -0,0 +1,12 @@ +package entities + +import "github.com/shopspring/decimal" + +type AlertID string + +type Alert struct { + ID AlertID + UserID UserID + Price decimal.Decimal + Instrument Instrument +} diff --git a/internal/repository/postgresql/alert.go b/internal/repository/postgresql/alert.go index 4e9a54a..c6c7baa 100644 --- a/internal/repository/postgresql/alert.go +++ b/internal/repository/postgresql/alert.go @@ -1 +1,115 @@ package postgresql + +import ( + "context" + "fmt" + + "gitea.computernetthings.ru/yash/crypto_alert_bot/internal/entities" + "github.com/shopspring/decimal" +) + +const saveAlertQuery = ` +insert into alert(user_id, instrument_id, price) +values ($1, $2, $3) +returning id` + +func (p *Postgresql) SaveAlert(ctx context.Context, alert *entities.Alert) (entities.AlertID, error) { + var id entities.AlertID + + err := p.db.QueryRow(ctx, saveAlertQuery, alert.UserID, alert.Instrument.ID, alert.Price).Scan(&id) + if err != nil { + return "", fmt.Errorf("failed to exec saveAlertQuery: %w", err) + } + + return id, nil +} + +const alertByIDQuery = ` +select a.id, a.user_id, a.price, i.id, c_base.symbol, c_quote.symbol +from alert a +join instrument i on i.id = a.instrument_id +join currency c_base on c_base.id = i.base_currency_id +join currency c_quote on c_quote.id = i.quoted_currency_id +where a.id = $1 and a.active = true` + +func (p *Postgresql) AlertByID(ctx context.Context, id entities.AlertID) (*entities.Alert, error) { + var alert entities.Alert + var priceStr string + + err := p.db.QueryRow(ctx, alertByIDQuery, id).Scan( + &alert.ID, &alert.UserID, &priceStr, + &alert.Instrument.ID, &alert.Instrument.BaseCurrency, &alert.Instrument.QuoteCurrency, + ) + if err != nil { + return nil, fmt.Errorf("failed to exec alertByIDQuery: %w", err) + } + + alert.Price, err = decimal.NewFromString(priceStr) + if err != nil { + return nil, fmt.Errorf("failed to parse alert price: %w", err) + } + + return &alert, nil +} + +const alertsByUserIDQuery = ` +select a.id, a.user_id, a.price, i.id, c_base.symbol, c_quote.symbol +from alert a +join instrument i on i.id = a.instrument_id +join currency c_base on c_base.id = i.base_currency_id +join currency c_quote on c_quote.id = i.quoted_currency_id +where a.user_id = $1 and a.active = true +order by a.id +offset $2 limit $3` + +func (p *Postgresql) AlertsByUserID(ctx context.Context, userID entities.UserID, offset, limit int) ([]entities.Alert, error) { + rows, err := p.db.Query(ctx, alertsByUserIDQuery, userID, offset, limit) + if err != nil { + return nil, fmt.Errorf("failed to exec alertsByUserIDQuery: %w", err) + } + defer rows.Close() + + var alerts []entities.Alert + for rows.Next() { + var alert entities.Alert + var priceStr string + + if err := rows.Scan( + &alert.ID, &alert.UserID, &priceStr, + &alert.Instrument.ID, &alert.Instrument.BaseCurrency, &alert.Instrument.QuoteCurrency, + ); err != nil { + return nil, fmt.Errorf("failed to scan alert row: %w", err) + } + + alert.Price, err = decimal.NewFromString(priceStr) + if err != nil { + return nil, fmt.Errorf("failed to parse alert price: %w", err) + } + + alerts = append(alerts, alert) + } + + return alerts, nil +} + +const deleteAlertQuery = "update alert set active = false where id = $1" + +func (p *Postgresql) DeleteAlert(ctx context.Context, id entities.AlertID) error { + _, err := p.db.Exec(ctx, deleteAlertQuery, id) + if err != nil { + return fmt.Errorf("failed to exec deleteAlertQuery: %w", err) + } + + return nil +} + +const updateAlertPriceQuery = "update alert set price = $2 where id = $1" + +func (p *Postgresql) UpdateAlertPrice(ctx context.Context, id entities.AlertID, price decimal.Decimal) error { + _, err := p.db.Exec(ctx, updateAlertPriceQuery, id, price.String()) + if err != nil { + return fmt.Errorf("failed to exec updateAlertPriceQuery: %w", err) + } + + return nil +} diff --git a/internal/repository/postgresql/migrations/000001_init.up.sql b/internal/repository/postgresql/migrations/000001_init.up.sql index bbabd71..6bba2f1 100644 --- a/internal/repository/postgresql/migrations/000001_init.up.sql +++ b/internal/repository/postgresql/migrations/000001_init.up.sql @@ -19,6 +19,7 @@ create table if not exists instrument ( create table if not exists alert ( id uuid primary key not null default gen_random_uuid(), + user_id uuid references users(id) not null, instrument_id uuid references instrument(id) not null, price text not null, active bool not null default true diff --git a/internal/repository/repository.go b/internal/repository/repository.go index e989202..0bc0ac5 100644 --- a/internal/repository/repository.go +++ b/internal/repository/repository.go @@ -4,6 +4,7 @@ import ( "context" "gitea.computernetthings.ru/yash/crypto_alert_bot/internal/entities" + "github.com/shopspring/decimal" ) type Storage interface { @@ -13,4 +14,10 @@ type Storage interface { InstrumentList(ctx context.Context, offset, limit int) ([]entities.Instrument, error) CreateInstrument(ctx context.Context, instrument *entities.Instrument) (entities.InstrumentID, error) + + SaveAlert(ctx context.Context, alert *entities.Alert) (entities.AlertID, error) + AlertByID(ctx context.Context, id entities.AlertID) (*entities.Alert, error) + AlertsByUserID(ctx context.Context, userID entities.UserID, offset, limit int) ([]entities.Alert, error) + DeleteAlert(ctx context.Context, id entities.AlertID) error + UpdateAlertPrice(ctx context.Context, id entities.AlertID, price decimal.Decimal) error } diff --git a/internal/usecase/alert.go b/internal/usecase/alert.go new file mode 100644 index 0000000..f7cf53b --- /dev/null +++ b/internal/usecase/alert.go @@ -0,0 +1,57 @@ +package usecase + +import ( + "context" + "fmt" + + "gitea.computernetthings.ru/yash/crypto_alert_bot/internal/entities" + "github.com/shopspring/decimal" +) + +func (uc *Usecase) CreateAlert(ctx context.Context, alert *entities.Alert) (entities.AlertID, error) { + id, err := uc.storage.SaveAlert(ctx, alert) + if err != nil { + uc.log.Error("failed to create alert", "alert", alert, "err", err) + return "", fmt.Errorf("failed to create alert: %w", err) + } + + return id, nil +} + +func (uc *Usecase) Alert(ctx context.Context, alertID entities.AlertID) (*entities.Alert, error) { + alert, err := uc.storage.AlertByID(ctx, alertID) + if err != nil { + uc.log.Error("failed to get alert", "alert_id", alertID, "err", err) + return nil, fmt.Errorf("failed to get alert: %w", err) + } + + return alert, nil +} + +func (uc *Usecase) Alerts(ctx context.Context, userID entities.UserID, offset, limit int) ([]entities.Alert, error) { + alerts, err := uc.storage.AlertsByUserID(ctx, userID, offset, limit) + if err != nil { + uc.log.Error("failed to get alerts", "user_id", userID, "err", err) + return nil, fmt.Errorf("failed to get alerts: %w", err) + } + + return alerts, nil +} + +func (uc *Usecase) RemoveAlert(ctx context.Context, alertID entities.AlertID) error { + if err := uc.storage.DeleteAlert(ctx, alertID); err != nil { + uc.log.Error("failed to remove alert", "alert_id", alertID, "err", err) + return fmt.Errorf("failed to remove alert: %w", err) + } + + return nil +} + +func (uc *Usecase) UpdateAlertPrice(ctx context.Context, alertID entities.AlertID, price decimal.Decimal) error { + if err := uc.storage.UpdateAlertPrice(ctx, alertID, price); err != nil { + uc.log.Error("failed to update alert price", "alert_id", alertID, "price", price, "err", err) + return fmt.Errorf("failed to update alert price: %w", err) + } + + return nil +}