forked from ebhomengo/niki
added mysql db
This commit is contained in:
parent
4ae65e95e1
commit
dbe9cb7df8
|
|
@ -2,20 +2,36 @@ package entity
|
|||
|
||||
import (
|
||||
"git.gocasts.ir/ebhomengo/niki/types"
|
||||
"time"
|
||||
)
|
||||
|
||||
type CartStatus string
|
||||
|
||||
type Item struct {
|
||||
ID types.ID
|
||||
CartID types.ID
|
||||
ProductID types.ID
|
||||
UserID types.ID
|
||||
Quantity int
|
||||
Price types.Price
|
||||
Price float64
|
||||
Name string
|
||||
AddedAt int64
|
||||
AddedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
}
|
||||
|
||||
type Cart struct {
|
||||
ID types.ID
|
||||
UserID types.ID
|
||||
Items []Item
|
||||
TotalPrice types.Price
|
||||
ExpireAt int64
|
||||
CreatedAt int64
|
||||
Status CartStatus
|
||||
TotalPrice float64
|
||||
ExpireAt time.Time
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
}
|
||||
|
||||
const (
|
||||
CartStatusActive CartStatus = "active"
|
||||
CartStatusExpired CartStatus = "expired"
|
||||
CartStatusCheckedOut CartStatus = "checked_out"
|
||||
)
|
||||
|
|
|
|||
|
|
@ -35,6 +35,10 @@ func New(client *redis.Client, cfg Config) Repo {
|
|||
return Repo{client: client, config: cfg}
|
||||
}
|
||||
|
||||
func op(s string) string {
|
||||
return fmt.Sprintf("shoppingbasketapp-repository-", s)
|
||||
}
|
||||
|
||||
func (r Repo) cartKey(userID types.ID) string {
|
||||
return r.config.KartKeyPrefix + fmt.Sprintf("%d", userID)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,18 @@
|
|||
package repository
|
||||
|
||||
import "errors"
|
||||
|
||||
const (
|
||||
FindCartByUserIDQuery = `SELECT id, user_id, total_price, status, expire_at, created_at
|
||||
FROM carts
|
||||
WHERE user_id = ? AND status = 'active' AND deleted_at IS NULL`
|
||||
|
||||
FindCartByIDQuery = `SELECT id, user_id, total_price, status, expire_at, created_at
|
||||
FROM carts
|
||||
WHERE id = ? AND status = 'active' AND deleted_at IS NULL`
|
||||
)
|
||||
|
||||
var (
|
||||
ErrNotExistsCart = errors.New("not exists cart")
|
||||
ErrExpiredCart = errors.New("expired cart")
|
||||
)
|
||||
|
|
@ -1,15 +1,19 @@
|
|||
-- +migrate Up
|
||||
CREATE TABLE `carts` (
|
||||
`id` INT PRIMARY KEY AUTO_INCREMENT,
|
||||
`user_id` INT NOT NULL,
|
||||
`id` BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
|
||||
`user_id` BIGINT UNSIGNED NOT NULL,
|
||||
`total_price` DECIMAL(10,2) DEFAULT 0,
|
||||
`expire_at` TIMESTAMP NULL,
|
||||
`status` ENUM('active', 'expired', 'checked_out') NOT NULL DEFAULT 'active',
|
||||
`expire_at` TIMESTAMP NOT NULL,
|
||||
`created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
`deleted_at` TIMESTAMP NULL DEFAULT NULL,
|
||||
`updated_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
CREATE INDEX `idx_carts_user_id` ON `carts`(`user_id`);
|
||||
CREATE INDEX `idx_carts_id` ON `carts`(`id`);
|
||||
|
||||
-- +migrate Down
|
||||
DROP INDEX `idx_carts_user_id` ON `carts`;
|
||||
DROP INDEX `idx_carts_id` ON `carts`;
|
||||
DROP TABLE `carts`;
|
||||
|
|
|
|||
|
|
@ -1,13 +1,16 @@
|
|||
-- +migrate Up
|
||||
CREATE TABLE `items` (
|
||||
`id` INT PRIMARY KEY AUTO_INCREMENT,
|
||||
`cart_id` INT NOT NULL,
|
||||
`product_id` INT NOT NULL,
|
||||
`id` BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
|
||||
`cart_id` BIGINT UNSIGNED NOT NULL,
|
||||
`product_id` BIGINT UNSIGNED NOT NULL,
|
||||
`user_id` BIGINT UNSIGNED NOT NULL,
|
||||
`quantity` INT NOT NULL DEFAULT 1,
|
||||
`price` DECIMAL(10,2) NOT NULL,
|
||||
`name` VARCHAR(256),
|
||||
`name` VARCHAR(256) NOT NULL ,
|
||||
`added_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (`cart_id`) REFERENCES `cart`(`id`)
|
||||
`deleted_at` TIMESTAMP NULL DEFAULT NULL,
|
||||
`updated_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (`cart_id`) REFERENCES `carts`(`id`)
|
||||
);
|
||||
|
||||
CREATE INDEX `idx_items_cart_id` ON `items`(`cart_id`);
|
||||
|
|
|
|||
|
|
@ -1,15 +1,408 @@
|
|||
package repository
|
||||
|
||||
import querier "git.gocasts.ir/ebhomengo/niki/pkg/query_transaction/sql"
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"errors"
|
||||
"fmt"
|
||||
"git.gocasts.ir/ebhomengo/niki/domain/shoppingbasket/entity"
|
||||
"git.gocasts.ir/ebhomengo/niki/pkg/database/mysql"
|
||||
richerror "git.gocasts.ir/ebhomengo/niki/pkg/rich_error"
|
||||
"git.gocasts.ir/ebhomengo/niki/types"
|
||||
"time"
|
||||
)
|
||||
|
||||
type DB struct {
|
||||
db *querier.SQLDB
|
||||
db *mysql.DB
|
||||
config Config
|
||||
}
|
||||
|
||||
func NewDB(db *querier.SQLDB) DB {
|
||||
func NewDB(db *mysql.DB) DB {
|
||||
return DB{db: db}
|
||||
}
|
||||
|
||||
func (d DB) addToBasket() {
|
||||
func (d DB) getCart(ctx context.Context, tx *sql.Tx, query string, by types.ID) (entity.Cart, error) {
|
||||
t, ok, err := d.getLocalTX(ctx, tx, "mysql-getCart")
|
||||
if err != nil {
|
||||
return entity.Cart{}, err
|
||||
}
|
||||
|
||||
if ok {
|
||||
defer func() {
|
||||
_ = tx.Rollback()
|
||||
}()
|
||||
}
|
||||
|
||||
var c entity.Cart
|
||||
|
||||
err = t.QueryRowContext(ctx, query, by).
|
||||
Scan(&c.ID, &c.UserID, &c.TotalPrice, &c.Status, &c.ExpireAt, &c.CreatedAt)
|
||||
if err != nil {
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return entity.Cart{}, richerror.New(richerror.Op(op("mysql-getCart"))).
|
||||
WithKind(richerror.KindNotFound).
|
||||
WithErr(ErrNotExistsCart)
|
||||
}
|
||||
|
||||
return entity.Cart{}, richerror.New(richerror.Op(op("mysql-getCart"))).
|
||||
WithKind(richerror.KindUnexpected).
|
||||
WithErr(err)
|
||||
|
||||
}
|
||||
|
||||
if c.ExpireAt.Before(time.Now().UTC()) {
|
||||
if err := d.updateCartStatus(ctx, t, c.ID, entity.CartStatusExpired); err != nil {
|
||||
return entity.Cart{}, err
|
||||
}
|
||||
return entity.Cart{}, richerror.New(richerror.Op(op("mysql-getCart"))).
|
||||
WithKind(richerror.KindInvalid).WithErr(ErrExpiredCart)
|
||||
}
|
||||
|
||||
itemsQuery := `SELECT id, cart_id, user_id, product_id, quantity, price, name, added_at, updated_at
|
||||
FROM items
|
||||
WHERE cart_id=? AND deleted_at IS NULL
|
||||
ORDER BY added_at`
|
||||
|
||||
rows, err := t.QueryContext(ctx, itemsQuery, c.ID)
|
||||
if err != nil {
|
||||
return entity.Cart{}, richerror.New(richerror.Op(op("mysql-getCart"))).
|
||||
WithKind(richerror.KindUnexpected).
|
||||
WithErr(err)
|
||||
}
|
||||
|
||||
defer rows.Close()
|
||||
|
||||
items := make([]entity.Item, 0)
|
||||
for rows.Next() {
|
||||
var i entity.Item
|
||||
if err := rows.Scan(&i.ID, &i.CartID, &i.UserID, &i.ProductID,
|
||||
&i.Quantity, &i.Price, &i.Name, &i.AddedAt, &i.UpdatedAt); err != nil {
|
||||
|
||||
return entity.Cart{}, richerror.New(richerror.Op(op("mysql-getCart"))).
|
||||
WithKind(richerror.KindUnexpected).
|
||||
WithErr(fmt.Errorf("error scan rows: %w", err))
|
||||
}
|
||||
|
||||
items = append(items, i)
|
||||
}
|
||||
|
||||
if err := rows.Err(); err != nil {
|
||||
return entity.Cart{}, richerror.New(richerror.Op(op("mysql-getCart"))).
|
||||
WithKind(richerror.KindUnexpected).
|
||||
WithErr(err)
|
||||
}
|
||||
|
||||
c.Items = items
|
||||
|
||||
if ok {
|
||||
if err := tx.Commit(); err != nil {
|
||||
return entity.Cart{}, richerror.New(richerror.Op(op("mysql-getCart"))).
|
||||
WithKind(richerror.
|
||||
KindUnexpected).WithErr(err)
|
||||
}
|
||||
}
|
||||
|
||||
return c, nil
|
||||
}
|
||||
|
||||
func (d DB) addToBasket(ctx context.Context, userID types.ID, item entity.Item) error {
|
||||
tx, err := d.db.Conn().BeginTx(ctx, &sql.TxOptions{
|
||||
Isolation: sql.LevelReadCommitted,
|
||||
})
|
||||
if err != nil {
|
||||
return richerror.New(richerror.Op(op("mysql-addToBasket"))).
|
||||
WithKind(richerror.KindUnexpected).WithErr(err)
|
||||
}
|
||||
|
||||
defer func() {
|
||||
_ = tx.Rollback()
|
||||
}()
|
||||
|
||||
c, err := d.getCart(ctx, tx, FindCartByUserIDQuery, userID)
|
||||
if err != nil {
|
||||
if err.Error() == ErrExpiredCart.Error() || err.Error() == ErrNotExistsCart.Error() {
|
||||
cartQuery := `INSERT INTO carts user_id, total_price, status, expire_at, created_at
|
||||
VALUES (?,?,?,?)`
|
||||
|
||||
newCart := entity.Cart{
|
||||
UserID: userID,
|
||||
TotalPrice: item.Price * float64(item.Quantity),
|
||||
Status: entity.CartStatusActive,
|
||||
CreatedAt: time.Now().UTC(),
|
||||
ExpireAt: time.Now().Add(d.config.TTL).UTC(),
|
||||
}
|
||||
|
||||
result, err := tx.ExecContext(ctx, cartQuery,
|
||||
newCart.UserID, newCart.TotalPrice, newCart.Status, newCart.ExpireAt, newCart.CreatedAt)
|
||||
if err != nil {
|
||||
return richerror.New(richerror.Op(op("mysql-addToBasket"))).
|
||||
WithKind(richerror.KindUnexpected).
|
||||
WithErr(err)
|
||||
}
|
||||
|
||||
cartID, _ := result.LastInsertId()
|
||||
item.CartID = types.ID(cartID)
|
||||
|
||||
if err := d.addItem(ctx, tx, item); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := d.updateTotalPrice(ctx, tx, types.ID(cartID)); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
isNew := true
|
||||
if c.ID > 0 {
|
||||
for _, i := range c.Items {
|
||||
if i.ProductID == item.ProductID {
|
||||
item.Quantity = i.Quantity + item.Quantity
|
||||
if err := d.updateQuantity(ctx, tx, item); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
isNew = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if isNew {
|
||||
err := d.addItem(ctx, tx, item)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if err := d.updateTotalPrice(ctx, tx, c.ID); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := tx.Commit(); err != nil {
|
||||
return richerror.New(richerror.Op(op("mysql-addToBasket"))).
|
||||
WithKind(richerror.KindUnexpected).
|
||||
WithErr(err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d DB) addItem(ctx context.Context, tx *sql.Tx, item entity.Item) error {
|
||||
t, ok, err := d.getLocalTX(ctx, tx, "mysql-addItem")
|
||||
if err != nil {
|
||||
return richerror.New(richerror.Op(op("mysql-addItem"))).
|
||||
WithKind(richerror.KindUnexpected).
|
||||
WithErr(err)
|
||||
}
|
||||
|
||||
if ok {
|
||||
defer func() {
|
||||
_ = tx.Rollback()
|
||||
}()
|
||||
}
|
||||
|
||||
query := `INSERT INTO items cart_id, user_id, product_id, quantity, price, name, added_at
|
||||
VALUES (?,?,?,?,?,?,?)`
|
||||
|
||||
_, err = t.ExecContext(ctx, query,
|
||||
item.CartID, item.UserID, item.ProductID, item.Quantity, item.Price, item.Name, item.AddedAt)
|
||||
if err != nil {
|
||||
return richerror.New(richerror.Op(op("mysql-addItem"))).
|
||||
WithKind(richerror.KindUnexpected).
|
||||
WithErr(err)
|
||||
}
|
||||
|
||||
if ok {
|
||||
if err := tx.Commit(); err != nil {
|
||||
return richerror.New(richerror.Op(op("mysql-addItem"))).
|
||||
WithKind(richerror.KindUnexpected).
|
||||
WithErr(err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d DB) updateQuantity(ctx context.Context, tx *sql.Tx, item entity.Item) error {
|
||||
t, ok, err := d.getLocalTX(ctx, tx, "mysql-updateQuantity")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if ok {
|
||||
defer func() {
|
||||
_ = tx.Rollback()
|
||||
}()
|
||||
}
|
||||
|
||||
query := `UPDATE items
|
||||
SET
|
||||
quantity = ?
|
||||
WHERE id = ?`
|
||||
|
||||
_, err = t.ExecContext(ctx, query, item.Quantity, item.ID)
|
||||
if err != nil {
|
||||
return richerror.New(richerror.Op(op("mysql-updateQuantity"))).
|
||||
WithKind(richerror.KindUnexpected).WithErr(err)
|
||||
}
|
||||
|
||||
if ok {
|
||||
if err := t.Commit(); err != nil {
|
||||
return richerror.New(richerror.Op(op("mysql-updateQuantity"))).
|
||||
WithKind(richerror.KindUnexpected).WithErr(err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d DB) updateTotalPrice(ctx context.Context, tx *sql.Tx, cartID types.ID) error {
|
||||
t, ok, err := d.getLocalTX(ctx, tx, "mysql-updateTotalPrice")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if ok {
|
||||
defer func() {
|
||||
_ = tx.Rollback()
|
||||
}()
|
||||
}
|
||||
|
||||
c, err := d.getCart(ctx, t, FindCartByIDQuery, cartID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var totalPrice float64
|
||||
|
||||
for _, i := range c.Items {
|
||||
totalPrice += i.Price * float64(i.Quantity)
|
||||
}
|
||||
|
||||
query := `UPDATE carts
|
||||
SET
|
||||
total_price = ?
|
||||
WHERE cart_id=?`
|
||||
|
||||
_, err = tx.ExecContext(ctx, query, totalPrice, c.ID)
|
||||
if err != nil {
|
||||
return richerror.New(richerror.Op(op("mysql-updateTotalPrice"))).
|
||||
WithKind(richerror.KindUnexpected).WithErr(err)
|
||||
}
|
||||
|
||||
if ok {
|
||||
if err := tx.Commit(); err != nil {
|
||||
return richerror.New(richerror.Op(op("mysql-updateTotalPrice"))).
|
||||
WithKind(richerror.KindUnexpected).WithErr(err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d DB) deleteItem(ctx context.Context, tx *sql.Tx, cartID, productID types.ID) error {
|
||||
t, ok, err := d.getLocalTX(ctx, tx, "mysql-deleteItem")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if ok {
|
||||
defer func() {
|
||||
_ = tx.Rollback()
|
||||
}()
|
||||
}
|
||||
|
||||
query := `UPDATE items
|
||||
SET
|
||||
deleted_at = ?
|
||||
WHERE cart_id = ? AND product_id = ? AND deleted_at IS NULL`
|
||||
deleteTime := time.Now().UTC()
|
||||
_, err = t.ExecContext(ctx, query, deleteTime, cartID, productID)
|
||||
if err != nil {
|
||||
return richerror.New(richerror.Op(op("mysql-deleteItem"))).
|
||||
WithKind(richerror.KindUnexpected).WithErr(err)
|
||||
}
|
||||
|
||||
if err := d.updateTotalPrice(ctx, t, cartID); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if ok {
|
||||
if err := t.Commit(); err != nil {
|
||||
return richerror.New(richerror.Op(op("mysql-deleteItem"))).
|
||||
WithKind(richerror.KindUnexpected).WithErr(err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d DB) deleteCart(ctx context.Context, cartID, userID types.ID) error {
|
||||
query := `UPDATE items
|
||||
SET
|
||||
deleted_at = ?
|
||||
WHERE cart_id = ? AND user_id = ? AND deleted_at IS NULL`
|
||||
deleteTime := time.Now().UTC()
|
||||
_, err := d.db.Conn().ExecContext(ctx, query, deleteTime, cartID, userID)
|
||||
if err != nil {
|
||||
return richerror.New(richerror.Op(op("mysql-deleteCart"))).
|
||||
WithKind(richerror.KindUnexpected).WithErr(err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d DB) updateCartStatus(ctx context.Context, tx *sql.Tx, cartID types.ID, status entity.CartStatus) error {
|
||||
t, ok, err := d.getLocalTX(ctx, tx, "mysql-updateCartStatus")
|
||||
if err != nil {
|
||||
return richerror.New(richerror.Op(op("mysql-updateCartStatus"))).
|
||||
WithKind(richerror.KindUnexpected).WithErr(err)
|
||||
}
|
||||
|
||||
if ok {
|
||||
defer func() {
|
||||
_ = t.Rollback()
|
||||
}()
|
||||
}
|
||||
|
||||
query := `UPDATE carts
|
||||
SET
|
||||
status = ?
|
||||
WHERE cart_id = ? AND deleted_at IS NULL`
|
||||
|
||||
_, err = t.ExecContext(ctx, query, status, cartID)
|
||||
if err != nil {
|
||||
return richerror.New(richerror.Op(op("mysql-updateCartStatus"))).
|
||||
WithKind(richerror.KindUnexpected).WithErr(err)
|
||||
}
|
||||
|
||||
if ok {
|
||||
if err := t.Commit(); err != nil {
|
||||
return richerror.New(richerror.Op(op("mysql-updateCartStatus"))).
|
||||
WithKind(richerror.KindUnexpected).WithErr(err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d DB) getLocalTX(ctx context.Context, tx *sql.Tx, p string) (*sql.Tx, bool, error) {
|
||||
var err error
|
||||
var ok bool
|
||||
if tx == nil {
|
||||
tx, err = d.db.Conn().BeginTx(ctx, &sql.TxOptions{
|
||||
Isolation: sql.LevelReadCommitted,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, false, richerror.New(richerror.Op(op(fmt.Sprintf("%s", p)))).
|
||||
WithKind(richerror.KindUnexpected).
|
||||
WithErr(err)
|
||||
}
|
||||
|
||||
ok = true
|
||||
}
|
||||
|
||||
return tx, ok, nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,9 +12,9 @@ import (
|
|||
type Repository interface {
|
||||
AddItem(ctx context.Context, userID types.ID, item entity.Item) error
|
||||
GetCart(ctx context.Context, userID types.ID) (entity.Cart, error)
|
||||
DeleteItem(ctx context.Context, userID, productID types.ID) error
|
||||
DeleteItem(ctx context.Context, cartID, itemID types.ID) error
|
||||
UpdateQuantity(ctx context.Context, userID, productID types.ID, quantity int) error
|
||||
DeleteCart(ctx context.Context, userID types.ID) error
|
||||
DeleteCart(ctx context.Context, cartID types.ID) error
|
||||
}
|
||||
|
||||
type Service struct {
|
||||
|
|
|
|||
Loading…
Reference in New Issue