Add user auth, cart, checkout and personal account for guests and registered users
This commit is contained in:
@@ -0,0 +1,176 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/jackc/pgx/v5"
|
||||
"github.com/jackc/pgx/v5/pgxpool"
|
||||
|
||||
"shop/internal/models"
|
||||
)
|
||||
|
||||
type CartRepository struct {
|
||||
pool *pgxpool.Pool
|
||||
}
|
||||
|
||||
func NewCartRepository(pool *pgxpool.Pool) *CartRepository {
|
||||
return &CartRepository{pool: pool}
|
||||
}
|
||||
|
||||
func (r *CartRepository) GetOrCreate(ctx context.Context, sessionKey string, userID *int) (int, error) {
|
||||
if userID != nil {
|
||||
var cartID int
|
||||
err := r.pool.QueryRow(ctx, `SELECT id FROM carts WHERE user_id = $1`, *userID).Scan(&cartID)
|
||||
if err == nil {
|
||||
return cartID, nil
|
||||
}
|
||||
if err != pgx.ErrNoRows {
|
||||
return 0, err
|
||||
}
|
||||
err = r.pool.QueryRow(ctx, `
|
||||
INSERT INTO carts (user_id, session_key) VALUES ($1, $2) RETURNING id
|
||||
`, *userID, sessionKey).Scan(&cartID)
|
||||
return cartID, err
|
||||
}
|
||||
|
||||
var cartID int
|
||||
err := r.pool.QueryRow(ctx, `
|
||||
SELECT id FROM carts WHERE session_key = $1 AND user_id IS NULL
|
||||
`, sessionKey).Scan(&cartID)
|
||||
if err == nil {
|
||||
return cartID, nil
|
||||
}
|
||||
if err != pgx.ErrNoRows {
|
||||
return 0, err
|
||||
}
|
||||
err = r.pool.QueryRow(ctx, `
|
||||
INSERT INTO carts (session_key) VALUES ($1) RETURNING id
|
||||
`, sessionKey).Scan(&cartID)
|
||||
return cartID, err
|
||||
}
|
||||
|
||||
func (r *CartRepository) MergeToUser(ctx context.Context, sessionKey string, userID int) error {
|
||||
tx, err := r.pool.Begin(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer tx.Rollback(ctx)
|
||||
|
||||
var guestCartID int
|
||||
err = tx.QueryRow(ctx, `
|
||||
SELECT id FROM carts WHERE session_key = $1 AND user_id IS NULL
|
||||
`, sessionKey).Scan(&guestCartID)
|
||||
if err == pgx.ErrNoRows {
|
||||
return tx.Commit(ctx)
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var userCartID int
|
||||
err = tx.QueryRow(ctx, `SELECT id FROM carts WHERE user_id = $1`, userID).Scan(&userCartID)
|
||||
if err == pgx.ErrNoRows {
|
||||
_, err = tx.Exec(ctx, `UPDATE carts SET user_id = $1 WHERE id = $2`, userID, guestCartID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return tx.Commit(ctx)
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = tx.Exec(ctx, `
|
||||
INSERT INTO cart_items (cart_id, product_id, quantity)
|
||||
SELECT $1, product_id, quantity FROM cart_items WHERE cart_id = $2
|
||||
ON CONFLICT (cart_id, product_id) DO UPDATE
|
||||
SET quantity = cart_items.quantity + EXCLUDED.quantity
|
||||
`, userCartID, guestCartID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = tx.Exec(ctx, `DELETE FROM carts WHERE id = $1`, guestCartID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return tx.Commit(ctx)
|
||||
}
|
||||
|
||||
func (r *CartRepository) AddItem(ctx context.Context, cartID, productID, qty int) error {
|
||||
_, err := r.pool.Exec(ctx, `
|
||||
INSERT INTO cart_items (cart_id, product_id, quantity)
|
||||
VALUES ($1, $2, $3)
|
||||
ON CONFLICT (cart_id, product_id) DO UPDATE
|
||||
SET quantity = cart_items.quantity + EXCLUDED.quantity
|
||||
`, cartID, productID, qty)
|
||||
return err
|
||||
}
|
||||
|
||||
func (r *CartRepository) UpdateItem(ctx context.Context, cartID, productID, qty int) error {
|
||||
if qty <= 0 {
|
||||
_, err := r.pool.Exec(ctx, `DELETE FROM cart_items WHERE cart_id = $1 AND product_id = $2`, cartID, productID)
|
||||
return err
|
||||
}
|
||||
_, err := r.pool.Exec(ctx, `
|
||||
UPDATE cart_items SET quantity = $3 WHERE cart_id = $1 AND product_id = $2
|
||||
`, cartID, productID, qty)
|
||||
return err
|
||||
}
|
||||
|
||||
func (r *CartRepository) RemoveItem(ctx context.Context, cartID, productID int) error {
|
||||
_, err := r.pool.Exec(ctx, `DELETE FROM cart_items WHERE cart_id = $1 AND product_id = $2`, cartID, productID)
|
||||
return err
|
||||
}
|
||||
|
||||
func (r *CartRepository) Clear(ctx context.Context, cartID int) error {
|
||||
_, err := r.pool.Exec(ctx, `DELETE FROM cart_items WHERE cart_id = $1`, cartID)
|
||||
return err
|
||||
}
|
||||
|
||||
func (r *CartRepository) ItemCount(ctx context.Context, cartID int) (int, error) {
|
||||
var count int
|
||||
err := r.pool.QueryRow(ctx, `
|
||||
SELECT COALESCE(SUM(quantity), 0) FROM cart_items WHERE cart_id = $1
|
||||
`, cartID).Scan(&count)
|
||||
return count, err
|
||||
}
|
||||
|
||||
func (r *CartRepository) Items(ctx context.Context, cartID int) ([]models.CartItem, error) {
|
||||
rows, err := r.pool.Query(ctx, `
|
||||
SELECT ci.id, ci.cart_id, ci.product_id, ci.quantity,
|
||||
p.id, p.name, p.description, p.details, p.price, p.image_url, p.category, p.is_active, p.created_at, p.updated_at
|
||||
FROM cart_items ci
|
||||
JOIN products p ON p.id = ci.product_id
|
||||
WHERE ci.cart_id = $1 AND p.is_active = true
|
||||
ORDER BY ci.id
|
||||
`, cartID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var items []models.CartItem
|
||||
for rows.Next() {
|
||||
var ci models.CartItem
|
||||
var p models.Product
|
||||
if err := rows.Scan(
|
||||
&ci.ID, &ci.CartID, &ci.ProductID, &ci.Quantity,
|
||||
&p.ID, &p.Name, &p.Description, &p.Details, &p.Price, &p.ImageURL, &p.Category, &p.IsActive, &p.CreatedAt, &p.UpdatedAt,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ci.Product = p
|
||||
items = append(items, ci)
|
||||
}
|
||||
return items, rows.Err()
|
||||
}
|
||||
|
||||
func (r *CartRepository) Total(ctx context.Context, cartID int) (float64, error) {
|
||||
var total float64
|
||||
err := r.pool.QueryRow(ctx, `
|
||||
SELECT COALESCE(SUM(ci.quantity * p.price), 0)
|
||||
FROM cart_items ci JOIN products p ON p.id = ci.product_id
|
||||
WHERE ci.cart_id = $1
|
||||
`, cartID).Scan(&total)
|
||||
return total, err
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/jackc/pgx/v5/pgxpool"
|
||||
|
||||
"shop/internal/models"
|
||||
)
|
||||
|
||||
type OrderRepository struct {
|
||||
pool *pgxpool.Pool
|
||||
}
|
||||
|
||||
func NewOrderRepository(pool *pgxpool.Pool) *OrderRepository {
|
||||
return &OrderRepository{pool: pool}
|
||||
}
|
||||
|
||||
func (r *OrderRepository) Create(ctx context.Context, order *models.Order, items []models.CartItem) error {
|
||||
tx, err := r.pool.Begin(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer tx.Rollback(ctx)
|
||||
|
||||
err = tx.QueryRow(ctx, `
|
||||
INSERT INTO orders (user_id, guest_name, guest_email, guest_phone, address, total, status)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, 'new')
|
||||
RETURNING id, created_at
|
||||
`, order.UserID, order.GuestName, order.GuestEmail, order.GuestPhone, order.Address, order.Total,
|
||||
).Scan(&order.ID, &order.CreatedAt)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, item := range items {
|
||||
_, err = tx.Exec(ctx, `
|
||||
INSERT INTO order_items (order_id, product_id, product_name, price, quantity)
|
||||
VALUES ($1, $2, $3, $4, $5)
|
||||
`, order.ID, item.ProductID, item.Product.Name, item.Product.Price, item.Quantity)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return tx.Commit(ctx)
|
||||
}
|
||||
|
||||
func (r *OrderRepository) ByUser(ctx context.Context, userID int) ([]models.Order, error) {
|
||||
rows, err := r.pool.Query(ctx, `
|
||||
SELECT id, user_id, guest_name, guest_email, guest_phone, address, total, status, created_at
|
||||
FROM orders WHERE user_id = $1 ORDER BY created_at DESC
|
||||
`, userID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var orders []models.Order
|
||||
for rows.Next() {
|
||||
var o models.Order
|
||||
if err := rows.Scan(&o.ID, &o.UserID, &o.GuestName, &o.GuestEmail, &o.GuestPhone, &o.Address, &o.Total, &o.Status, &o.CreatedAt); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
o.Items, err = r.items(ctx, o.ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
orders = append(orders, o)
|
||||
}
|
||||
return orders, rows.Err()
|
||||
}
|
||||
|
||||
func (r *OrderRepository) items(ctx context.Context, orderID int) ([]models.OrderItem, error) {
|
||||
rows, err := r.pool.Query(ctx, `
|
||||
SELECT id, order_id, product_id, product_name, price, quantity
|
||||
FROM order_items WHERE order_id = $1
|
||||
`, orderID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var items []models.OrderItem
|
||||
for rows.Next() {
|
||||
var i models.OrderItem
|
||||
if err := rows.Scan(&i.ID, &i.OrderID, &i.ProductID, &i.ProductName, &i.Price, &i.Quantity); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, i)
|
||||
}
|
||||
return items, rows.Err()
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
|
||||
"github.com/jackc/pgx/v5"
|
||||
"github.com/jackc/pgx/v5/pgxpool"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
|
||||
"shop/internal/models"
|
||||
)
|
||||
|
||||
type UserRepository struct {
|
||||
pool *pgxpool.Pool
|
||||
}
|
||||
|
||||
func NewUserRepository(pool *pgxpool.Pool) *UserRepository {
|
||||
return &UserRepository{pool: pool}
|
||||
}
|
||||
|
||||
func (r *UserRepository) Create(ctx context.Context, email, password, name string) (models.User, error) {
|
||||
hash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
|
||||
if err != nil {
|
||||
return models.User{}, err
|
||||
}
|
||||
|
||||
var u models.User
|
||||
err = r.pool.QueryRow(ctx, `
|
||||
INSERT INTO users (email, password_hash, name)
|
||||
VALUES ($1, $2, $3)
|
||||
RETURNING id, email, name, phone, address, created_at
|
||||
`, email, string(hash), name).Scan(&u.ID, &u.Email, &u.Name, &u.Phone, &u.Address, &u.CreatedAt)
|
||||
return u, err
|
||||
}
|
||||
|
||||
func (r *UserRepository) Authenticate(ctx context.Context, email, password string) (models.User, error) {
|
||||
var hash string
|
||||
var u models.User
|
||||
err := r.pool.QueryRow(ctx, `
|
||||
SELECT id, email, name, phone, address, created_at, password_hash
|
||||
FROM users WHERE email = $1
|
||||
`, email).Scan(&u.ID, &u.Email, &u.Name, &u.Phone, &u.Address, &u.CreatedAt, &hash)
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
return models.User{}, pgx.ErrNoRows
|
||||
}
|
||||
if err != nil {
|
||||
return models.User{}, err
|
||||
}
|
||||
if bcrypt.CompareHashAndPassword([]byte(hash), []byte(password)) != nil {
|
||||
return models.User{}, pgx.ErrNoRows
|
||||
}
|
||||
return u, nil
|
||||
}
|
||||
|
||||
func (r *UserRepository) GetByID(ctx context.Context, id int) (models.User, error) {
|
||||
var u models.User
|
||||
err := r.pool.QueryRow(ctx, `
|
||||
SELECT id, email, name, phone, address, created_at FROM users WHERE id = $1
|
||||
`, id).Scan(&u.ID, &u.Email, &u.Name, &u.Phone, &u.Address, &u.CreatedAt)
|
||||
return u, err
|
||||
}
|
||||
|
||||
func (r *UserRepository) UpdateProfile(ctx context.Context, u *models.User) error {
|
||||
_, err := r.pool.Exec(ctx, `
|
||||
UPDATE users SET name = $1, phone = $2, address = $3 WHERE id = $4
|
||||
`, u.Name, u.Phone, u.Address, u.ID)
|
||||
return err
|
||||
}
|
||||
|
||||
func (r *UserRepository) EmailExists(ctx context.Context, email string) (bool, error) {
|
||||
var exists bool
|
||||
err := r.pool.QueryRow(ctx, `SELECT EXISTS(SELECT 1 FROM users WHERE email = $1)`, email).Scan(&exists)
|
||||
return exists, err
|
||||
}
|
||||
Reference in New Issue
Block a user