Skip to content

Commit

Permalink
Init
Browse files Browse the repository at this point in the history
  • Loading branch information
Пешков Дмитрий committed Sep 5, 2024
1 parent 5a440dc commit b72e9cb
Show file tree
Hide file tree
Showing 22 changed files with 1,284 additions and 1 deletion.
15 changes: 14 additions & 1 deletion cmd/gophermart/main.go
Original file line number Diff line number Diff line change
@@ -1,3 +1,16 @@
package main

func main() {}
import (
"github.com/spqrcor/gofermart/internal/config"
"github.com/spqrcor/gofermart/internal/db"
"github.com/spqrcor/gofermart/internal/logger"
"github.com/spqrcor/gofermart/internal/server"
)

func main() {
config.Init()
logger.Init()
db.Init()

server.Start()
}
74 changes: 74 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
module github.com/spqrcor/gofermart

go 1.22

require (
filippo.io/edwards25519 v1.1.0 // indirect
github.com/ClickHouse/ch-go v0.62.0 // indirect
github.com/ClickHouse/clickhouse-go/v2 v2.28.1 // indirect
github.com/andybalholm/brotli v1.1.0 // indirect
github.com/antlr4-go/antlr/v4 v4.13.1 // indirect
github.com/coder/websocket v1.8.12 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/elastic/go-sysinfo v1.14.1 // indirect
github.com/elastic/go-windows v1.0.2 // indirect
github.com/go-chi/chi/v5 v5.1.0 // indirect
github.com/go-faster/city v1.0.1 // indirect
github.com/go-faster/errors v0.7.1 // indirect
github.com/go-sql-driver/mysql v1.8.1 // indirect
github.com/golang-jwt/jwt/v4 v4.5.0 // indirect
github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 // indirect
github.com/golang-sql/sqlexp v0.1.0 // indirect
github.com/golang/protobuf v1.5.4 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
github.com/jackc/pgx/v5 v5.6.0 // indirect
github.com/jackc/puddle/v2 v2.2.1 // indirect
github.com/joeshaw/multierror v0.0.0-20140124173710-69b34d4ec901 // indirect
github.com/jonboulle/clockwork v0.4.0 // indirect
github.com/klauspost/compress v1.17.9 // indirect
github.com/libsql/sqlite-antlr4-parser v0.0.0-20240721121621-c0bdc870f11c // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mfridman/interpolate v0.0.2 // indirect
github.com/microsoft/go-mssqldb v1.7.2 // indirect
github.com/ncruces/go-strftime v0.1.9 // indirect
github.com/paulmach/orb v0.11.1 // indirect
github.com/pierrec/lz4/v4 v4.1.21 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/pressly/goose/v3 v3.21.1 // indirect
github.com/prometheus/procfs v0.15.1 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
github.com/segmentio/asm v1.2.0 // indirect
github.com/sethvargo/go-retry v0.3.0 // indirect
github.com/shopspring/decimal v1.4.0 // indirect
github.com/tursodatabase/libsql-client-go v0.0.0-20240812094001-348a4e45b535 // indirect
github.com/vertica/vertica-sql-go v1.3.3 // indirect
github.com/ydb-platform/ydb-go-genproto v0.0.0-20240821162910-6cb364b2ccc8 // indirect
github.com/ydb-platform/ydb-go-sdk/v3 v3.77.1 // indirect
github.com/ziutek/mymysql v1.5.4 // indirect
go.opentelemetry.io/otel v1.29.0 // indirect
go.opentelemetry.io/otel/trace v1.29.0 // indirect
go.uber.org/multierr v1.11.0 // indirect
go.uber.org/zap v1.27.0 // indirect
golang.org/x/crypto v0.26.0 // indirect
golang.org/x/exp v0.0.0-20240823005443-9b4947da3948 // indirect
golang.org/x/net v0.28.0 // indirect
golang.org/x/sync v0.8.0 // indirect
golang.org/x/sys v0.24.0 // indirect
golang.org/x/text v0.17.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20240827150818-7e3bb234dfed // indirect
google.golang.org/grpc v1.66.0 // indirect
google.golang.org/protobuf v1.34.2 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
howett.net/plist v1.0.1 // indirect
modernc.org/gc/v3 v3.0.0-20240801135723-a856999a2e4a // indirect
modernc.org/libc v1.60.0 // indirect
modernc.org/mathutil v1.6.0 // indirect
modernc.org/memory v1.8.0 // indirect
modernc.org/sqlite v1.32.0 // indirect
modernc.org/strutil v1.2.0 // indirect
modernc.org/token v1.1.0 // indirect
nhooyr.io/websocket v1.8.17 // indirect
)
311 changes: 311 additions & 0 deletions go.sum

Large diffs are not rendered by default.

53 changes: 53 additions & 0 deletions internal/actions/add_orders.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package actions

import (
"context"
"fmt"
"github.com/google/uuid"
"github.com/spqrcor/gofermart/internal/authenticate"
"github.com/spqrcor/gofermart/internal/db"
"time"
)

var ErrOrderAnotherUserExists = fmt.Errorf("order another user exists")
var ErrOrderUserExists = fmt.Errorf("order user exists")
var ErrOrderInvalidFormat = fmt.Errorf("order invalid format")

func AddOrder(ctx context.Context, orderNum string) error {
if !isNumberValid(orderNum) {
return ErrOrderInvalidFormat
}

var baseUserID, baseOrderID string
orderId := uuid.NewString()
userId := ctx.Value(authenticate.ContextUserID)
childCtx, cancel := context.WithTimeout(ctx, time.Second*3)
defer cancel()
err := db.Source.QueryRowContext(childCtx, "INSERT INTO orders (id, user_id, number) VALUES ($1, $2, $3) "+
"ON CONFLICT(number) DO UPDATE SET number = EXCLUDED.number RETURNING id, user_id", orderId, userId, orderNum).Scan(&baseOrderID, &baseUserID)
if err != nil {
return err
} else if userId != uuid.MustParse(baseUserID) {
return ErrOrderAnotherUserExists
} else if orderId != baseOrderID {
return ErrOrderUserExists
}
return nil
}

func isNumberValid(orderNum string) bool {
total := 0
isSecondDigit := false
for i := len(orderNum) - 1; i >= 0; i-- {
digit := int(orderNum[i] - '0')
if isSecondDigit {
digit *= 2
if digit > 9 {
digit -= 9
}
}
total += digit
isSecondDigit = !isSecondDigit
}
return total%10 == 0
}
57 changes: 57 additions & 0 deletions internal/actions/add_withdraw.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package actions

import (
"context"
"database/sql"
"errors"
"fmt"
"github.com/jackc/pgx/v5/pgconn"
"github.com/spqrcor/gofermart/internal/authenticate"
"github.com/spqrcor/gofermart/internal/db"
"time"
)

type InputWithdraw struct {
OrderNum string `json:"order"`
Sum int `json:"sum"`
}

var ErrBalance = fmt.Errorf("balance error")

func AddWithdraw(ctx context.Context, input InputWithdraw) error {
if !isNumberValid(input.OrderNum) {
return ErrOrderInvalidFormat
}
userId := ctx.Value(authenticate.ContextUserID)

childCtx, cancel := context.WithTimeout(ctx, time.Second*3)
defer cancel()

tx, err := db.Source.BeginTx(childCtx, nil)
if err != nil {
return err
}
_, err = tx.ExecContext(childCtx, "UPDATE users SET balance = balance - $2 WHERE id = $1", userId, input.Sum)
if err != nil {
_ = tx.Rollback()
var pgError *pgconn.PgError
if errors.As(err, &pgError) && pgError.ConstraintName == "users_balance_check" {
return ErrBalance
}
return err
}

var withdrawID string
err = tx.QueryRowContext(childCtx, "INSERT INTO withdraw_list (order_id, sum) SELECT id, $3 FROM orders WHERE user_id = $1 AND number = $2 RETURNING id", userId, input.OrderNum, input.Sum).Scan(&withdrawID)
if err != nil {
_ = tx.Rollback()
if errors.Is(err, sql.ErrNoRows) {
return ErrOrderInvalidFormat
}
return err
}
if err = tx.Commit(); err != nil {
return err
}
return nil
}
27 changes: 27 additions & 0 deletions internal/actions/get_balance.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package actions

import (
"context"
"errors"
"github.com/spqrcor/gofermart/internal/authenticate"
"github.com/spqrcor/gofermart/internal/db"
"time"
)

type BalanceInfo struct {
Current int `json:"current"`
Withdrawn int `json:"withdrawn"`
}

func GetBalanceInfo(ctx context.Context) (BalanceInfo, error) {
balanceInfo := BalanceInfo{}
childCtx, cancel := context.WithTimeout(ctx, time.Second*3)
defer cancel()

row := db.Source.QueryRowContext(childCtx, "SELECT balance, (SELECT COALESCE(SUM(wl.sum), 0) FROM withdraw_list wl INNER JOIN orders o ON o.id = wl.order_id WHERE o.user_id = u.id) "+
"FROM users u WHERE id = $1", ctx.Value(authenticate.ContextUserID))
if err := row.Scan(&balanceInfo.Current, &balanceInfo.Withdrawn); err != nil {
return balanceInfo, errors.New("User not found")
}
return balanceInfo, nil
}
54 changes: 54 additions & 0 deletions internal/actions/get_orders.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package actions

import (
"context"
"database/sql"
"fmt"
"github.com/spqrcor/gofermart/internal/authenticate"
"github.com/spqrcor/gofermart/internal/db"
"github.com/spqrcor/gofermart/internal/logger"
"time"
)

type Order struct {
Number string `json:"number"`
Status string `json:"status"`
Accrual int `json:"accrual,omitempty"`
UploadedAt string `json:"uploaded_at"`
}

var ErrOrdersNotFound = fmt.Errorf("orders not found")

func GetOrders(ctx context.Context) ([]Order, error) {
var orders []Order
childCtx, cancel := context.WithTimeout(ctx, time.Second*3)
defer cancel()

rows, err := db.Source.QueryContext(childCtx, "SELECT number, status, accrual, created_at FROM orders WHERE user_id = $1 ORDER BY created_at DESC", ctx.Value(authenticate.ContextUserID))
if err != nil {
return nil, err
}
defer func() {
if err := rows.Close(); err != nil {
logger.Log.Error(err.Error())
}
if err := rows.Err(); err != nil {
logger.Log.Error(err.Error())
}
}()

for rows.Next() {
o := Order{}
var accrual sql.NullInt32
if err = rows.Scan(&o.Number, &o.Status, &accrual, &o.UploadedAt); err != nil {
return nil, err
}
o.Accrual = int(accrual.Int32)
orders = append(orders, o)
}

if len(orders) == 0 {
return nil, ErrOrdersNotFound
}
return orders, nil
}
50 changes: 50 additions & 0 deletions internal/actions/get_withdrawals.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package actions

import (
"context"
"fmt"
"github.com/spqrcor/gofermart/internal/authenticate"
"github.com/spqrcor/gofermart/internal/db"
"github.com/spqrcor/gofermart/internal/logger"
"time"
)

type Withdraw struct {
OrderNum string `json:"order"`
Sum int `json:"sum"`
ProcessedAt string `json:"processed_at"`
}

var ErrWithdrawNotFound = fmt.Errorf("withdraw not found")

func GetWithdraws(ctx context.Context) ([]Withdraw, error) {
var withdraws []Withdraw
childCtx, cancel := context.WithTimeout(ctx, time.Second*3)
defer cancel()

rows, err := db.Source.QueryContext(childCtx, "SELECT o.number, wl.sum, wl.created_at FROM withdraw_list wl "+
"INNER JOIN orders o ON o.id = wl.order_id WHERE o.user_id = $1 ORDER BY wl.created_at DESC", ctx.Value(authenticate.ContextUserID))
if err != nil {
return nil, err
}
defer func() {
if err := rows.Close(); err != nil {
logger.Log.Error(err.Error())
}
if err := rows.Err(); err != nil {
logger.Log.Error(err.Error())
}
}()

for rows.Next() {
w := Withdraw{}
if err = rows.Scan(&w.OrderNum, &w.Sum, &w.ProcessedAt); err != nil {
return nil, err
}
withdraws = append(withdraws, w)
}
if len(withdraws) == 0 {
return nil, ErrWithdrawNotFound
}
return withdraws, nil
}
33 changes: 33 additions & 0 deletions internal/actions/login.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package actions

import (
"context"
"database/sql"
"errors"
"fmt"
"github.com/google/uuid"
"github.com/spqrcor/gofermart/internal/db"
"golang.org/x/crypto/bcrypt"
"time"
)

var ErrLogin = fmt.Errorf("login error")

func Login(ctx context.Context, input InputDataUser) (uuid.UUID, error) {
childCtx, cancel := context.WithTimeout(ctx, time.Second*3)
defer cancel()
row := db.Source.QueryRowContext(childCtx, "SELECT id, password FROM users WHERE login = $1", input.Login)

var userID, password string
err := row.Scan(&userID, &password)
if errors.Is(err, sql.ErrNoRows) {
return uuid.Nil, ErrLogin
} else if err != nil {
return uuid.Nil, err
}

if err := bcrypt.CompareHashAndPassword([]byte(password), []byte(input.Password)); err != nil {
return uuid.Nil, ErrLogin
}
return uuid.MustParse(userID), nil
}
Loading

0 comments on commit b72e9cb

Please sign in to comment.