niki/domain/shoppingbasket/repository/mysql.go

424 lines
10 KiB
Go

package repository
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 *mysql.DB
config Config
}
func NewDB(db *mysql.DB, cfg Config) DB {
return DB{db: db, config: cfg}
}
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.MysqlTTL).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, itemID 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 id = ? AND deleted_at IS NULL`
deleteTime := time.Now().UTC()
_, err = t.ExecContext(ctx, query, deleteTime, cartID, itemID)
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 {
itemQuery := `UPDATE items
SET
deleted_at = ?
WHERE cart_id = ? AND user_id = ? AND deleted_at IS NULL`
deleteItemTime := time.Now().UTC()
_, err := d.db.Conn().ExecContext(ctx, itemQuery, deleteItemTime, cartID, userID)
if err != nil {
return richerror.New(richerror.Op(op("mysql-deleteCart"))).
WithKind(richerror.KindUnexpected).WithErr(err)
}
cartQuery := `UPDATE carts
SET
deleted_at = ?
WHERE id = ? AND user_id = ? AND deleted_at IS NULL`
deleteCartTime := time.Now().UTC()
_, err = d.db.Conn().ExecContext(ctx, cartQuery, deleteCartTime, 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 id = ? AND status = active 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
}