forked from ebhomengo/niki
424 lines
10 KiB
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) DB {
|
|
return DB{db: db}
|
|
}
|
|
|
|
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
|
|
}
|