Skip to content

Commit 24cd33a

Browse files
author
Armin
committed
refactor database connection and balance management logic
1 parent 5998407 commit 24cd33a

File tree

6 files changed

+156
-52
lines changed

6 files changed

+156
-52
lines changed

app/app.go

Lines changed: 11 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,19 @@
11
package app
22

33
import (
4-
"fmt"
54
"log/slog"
65
"os"
6+
"sms-gateway/config"
7+
"sms-gateway/pkg/db"
78

89
_ "github.com/go-sql-driver/mysql"
9-
"github.com/jmoiron/sqlx"
1010
"github.com/labstack/echo/v4"
1111
)
1212

1313
var (
1414
Echo *echo.Echo
1515
Logger *slog.Logger
16-
DB *sqlx.DB
16+
DB *db.DB
1717
)
1818

1919
func Init() {
@@ -27,43 +27,20 @@ func initLogger() {
2727
Logger = slog.New(handler)
2828
}
2929

30-
type dbConfig struct {
31-
Username string
32-
Password string
33-
Host string
34-
Port int
35-
DBName string
36-
}
37-
38-
func ConnectionString(dbConfig dbConfig) string {
39-
url := fmt.Sprintf("%s:%s@tcp(%s:%d)/%s",
40-
dbConfig.Username,
41-
dbConfig.Password,
42-
dbConfig.Host,
43-
dbConfig.Port,
44-
dbConfig.DBName)
45-
return url
46-
}
47-
4830
func initDB() {
4931
var err error
50-
DB, err = sqlx.Open("mysql", ConnectionString(dbConfig{
51-
Username: "sms_user",
52-
Password: "sms_pass",
53-
Host: "localhost",
54-
Port: 3306,
55-
DBName: "sms_gateway",
56-
}))
32+
DB, err = db.ConnectDB(db.Config{
33+
Username: config.DBUsername,
34+
Password: config.DBPassword,
35+
Host: config.DBHost,
36+
Port: config.DBPort,
37+
DBName: config.DBName,
38+
})
5739
if err != nil {
5840
panic(err)
5941
}
60-
61-
if err := DB.Ping(); err != nil {
62-
panic(err)
63-
}
64-
6542
// migrate
66-
43+
// seed
6744
}
6845

6946
func initEcho() {

config/config.go

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,31 @@
11
package config
22

3-
import "sms-gateway/pkg/env"
3+
import (
4+
"sms-gateway/pkg/env"
5+
"strconv"
6+
)
47

58
var (
69
AppName string
710
AppBaseURL string
811
AppListenAddr string
12+
DBUsername string
13+
DBPassword string
14+
DBHost string
15+
DBPort int
16+
DBName string
917
)
1018

1119
func init() {
1220
AppName = env.Default("APP_NAME", "sms-gateway")
1321
AppListenAddr = env.RequiredNotEmpty("LISTEN_ADDR")
22+
DBUsername = env.RequiredNotEmpty("DB_USER_NAME")
23+
DBPassword = env.RequiredNotEmpty("DB_PASSWORD")
24+
DBHost = env.RequiredNotEmpty("DB_HOST")
25+
port, err := strconv.Atoi(env.RequiredNotEmpty("DB_PORT"))
26+
if err != nil {
27+
panic("invalid DB_PORT: " + err.Error())
28+
}
29+
DBPort = port
30+
DBName = env.RequiredNotEmpty("DB_NAME")
1431
}

internal/balance/db/db.sql

Lines changed: 5 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -11,15 +11,9 @@ values (1,100);
1111

1212

1313
CREATE TABLE user_transactions (
14-
transaction_id BIGINT AUTO_INCREMENT PRIMARY KEY,
15-
user_id BIGINT NOT NULL,
16-
amount BIGINT NOT NULL, -- Positive for deposit, negative for withdrawal
17-
new_balance BIGINT NOT NULL, -- Balance after this transaction
18-
transaction_type VARCHAR(50) NOT NULL, -- e.g., 'deposit', 'withdrawal', 'transfer'
19-
description TEXT,
20-
created_at DATETIME(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),
21-
22-
INDEX idx_user_id_created_at (user_id, created_at DESC),
23-
INDEX idx_transaction_type (transaction_type),
24-
FOREIGN KEY (user_id) REFERENCES user_balances(user_id) ON DELETE CASCADE
14+
user_id BIGINT NOT NULL,
15+
amount BIGINT NOT NULL, -- Positive for deposit, negative for withdrawal
16+
transaction_type VARCHAR(50) NOT NULL, -- e.g., 'deposit', 'withdrawal', 'transfer'
17+
description TEXT,
18+
created_at DATETIME(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),
2519
) ENGINE=InnoDB;

internal/balance/service.go

Lines changed: 62 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,17 +4,24 @@ import (
44
"context"
55
"database/sql"
66
"errors"
7+
"fmt"
78
"sms-gateway/app"
89
"sms-gateway/internal/model"
910
)
1011

12+
type transactionType string
13+
14+
const (
15+
Withdrawal transactionType = "withdrawal"
16+
)
17+
1118
type UserHasEnoughBalanceRequest struct {
1219
CustomerID int64
1320
Quantity int
1421
Type model.Type
1522
}
1623

17-
func UserHasEnoughBalance(ctx context.Context, req UserHasEnoughBalanceRequest) (bool, error) {
24+
func UserHasBalance(ctx context.Context, req UserHasEnoughBalanceRequest) (bool, error) {
1825

1926
const query = `SELECT balance FROM user_balances WHERE user_id = ?`
2027
var balance int64
@@ -29,6 +36,56 @@ func UserHasEnoughBalance(ctx context.Context, req UserHasEnoughBalanceRequest)
2936
return balance >= price, nil
3037
}
3138

39+
type DeductBalanceRequest struct {
40+
CustomerID int64
41+
Quantity int
42+
Type model.Type
43+
}
44+
45+
func DeductBalance(ctx context.Context, req DeductBalanceRequest) (err error) {
46+
price := calculatePrice(req.Type, req.Quantity)
47+
48+
tx, err := app.DB.DB.BeginTxx(ctx, nil)
49+
if err != nil {
50+
return err
51+
}
52+
defer func() {
53+
if err != nil {
54+
_ = tx.Rollback()
55+
}
56+
}()
57+
58+
const updateBalanceQuery = `UPDATE user_balances SET balance = balance - ? WHERE user_id = ? AND balance >= ?`
59+
res, err := tx.ExecContext(ctx, updateBalanceQuery, price, req.CustomerID, price)
60+
if err != nil {
61+
return err
62+
}
63+
64+
rows, err := res.RowsAffected()
65+
if err != nil {
66+
return err
67+
}
68+
if rows == 0 {
69+
return errors.New("insufficient balance")
70+
}
71+
72+
const insertTransactionQuery = `INSERT INTO user_transactions (user_id, amount, transaction_type, description) VALUES (?, ?, ?, ?)`
73+
if _, err = tx.ExecContext(ctx,
74+
insertTransactionQuery,
75+
req.CustomerID,
76+
-price,
77+
Withdrawal,
78+
descriptionGenerator(req.Type, req.Quantity)); err != nil {
79+
return err
80+
}
81+
82+
if err = tx.Commit(); err != nil {
83+
return err
84+
}
85+
86+
return nil
87+
}
88+
3289
func calculatePrice(Type model.Type, Quantity int) int64 {
3390
return int64(getPricePerType(Type) * Quantity)
3491
}
@@ -40,3 +97,7 @@ func getPricePerType(t model.Type) int {
4097
}
4198
return 1
4299
}
100+
101+
func descriptionGenerator(t model.Type, q int) string {
102+
return fmt.Sprintf("بابت خرید %d پیامک تایپ %s", q, t)
103+
}

internal/sms/handler.go

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -25,24 +25,29 @@ func SendHandler(c echo.Context) error {
2525
app.Logger.Info("", "s", s)
2626

2727
// check user balance
28-
hasBalance, err := balance.UserHasEnoughBalance(c.Request().Context(), balance.UserHasEnoughBalanceRequest{
28+
hasBalance, err := balance.UserHasBalance(c.Request().Context(), balance.UserHasEnoughBalanceRequest{
2929
CustomerID: s.CustomerID,
3030
Quantity: len(s.Recipients),
3131
Type: s.Type,
3232
})
3333
if err != nil {
34-
app.Logger.Error("UserHasEnoughBalance ", "err", err)
34+
app.Logger.Error("UserHasBalance ", "err", err)
3535
return c.JSON(http.StatusInternalServerError, map[string]string{"error": "internal error"})
3636
}
3737
if !hasBalance {
3838
app.Logger.Info("User Has Not Enough Balance ", "user id ", s.CustomerID)
3939
return c.JSON(http.StatusPaymentRequired, map[string]string{"error": "dont have "})
4040
}
4141

42-
app.Logger.Info("user has Enough Balance", "s", s)
42+
if err := balance.DeductBalance(c.Request().Context(), balance.DeductBalanceRequest{
43+
CustomerID: s.CustomerID,
44+
Quantity: len(s.Recipients),
45+
Type: s.Type,
46+
}); err != nil {
47+
app.Logger.Error("DeductBalance ", "err", err)
48+
return c.JSON(http.StatusInternalServerError, map[string]string{"error": "internal error"})
49+
}
4350

44-
// if not ok return err
45-
//else cut down the balance
4651
//then send it in quew
4752

4853
return c.JSON(http.StatusOK, nil)

pkg/db/db.go

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
package db
2+
3+
import (
4+
"fmt"
5+
6+
_ "github.com/go-sql-driver/mysql"
7+
8+
"github.com/jmoiron/sqlx"
9+
)
10+
11+
type Config struct {
12+
Username string
13+
Password string
14+
Host string
15+
Port int
16+
DBName string
17+
}
18+
19+
type DB struct {
20+
*sqlx.DB
21+
}
22+
23+
func ConnectionString(dbConfig Config) string {
24+
url := fmt.Sprintf("%s:%s@tcp(%s:%d)/%s",
25+
dbConfig.Username,
26+
dbConfig.Password,
27+
dbConfig.Host,
28+
dbConfig.Port,
29+
dbConfig.DBName)
30+
return url
31+
}
32+
33+
func ConnectDB(config Config) (*DB, error) {
34+
db, err := sqlx.Open("mysql", ConnectionString(Config{
35+
Username: config.Username,
36+
Password: config.Password,
37+
Host: config.Host,
38+
Port: config.Port,
39+
DBName: config.DBName,
40+
}))
41+
if err != nil {
42+
return nil, err
43+
}
44+
45+
if err := db.Ping(); err != nil {
46+
return nil, err
47+
}
48+
49+
return &DB{db}, nil
50+
}

0 commit comments

Comments
 (0)