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 }