v0.30: product pages, categories, sale prices and order status timeline
This commit is contained in:
@@ -2,8 +2,8 @@ package repository
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/jackc/pgx/v5"
|
||||
"github.com/jackc/pgx/v5/pgxpool"
|
||||
@@ -19,25 +19,32 @@ func NewProductRepository(pool *pgxpool.Pool) *ProductRepository {
|
||||
return &ProductRepository{pool: pool}
|
||||
}
|
||||
|
||||
const productSelect = `
|
||||
SELECT p.id, p.name, p.description, p.details, p.price, COALESCE(p.sale_price, 0),
|
||||
p.image_url, COALESCE(c.name, p.category, 'Разное'), p.category_id,
|
||||
COALESCE(c.slug, ''), p.is_active, p.created_at, p.updated_at
|
||||
FROM products p
|
||||
LEFT JOIN categories c ON c.id = p.category_id
|
||||
`
|
||||
|
||||
func (r *ProductRepository) scanProduct(row pgx.Row) (models.Product, error) {
|
||||
var p models.Product
|
||||
var catID sql.NullInt64
|
||||
err := row.Scan(
|
||||
&p.ID, &p.Name, &p.Description, &p.Details, &p.Price,
|
||||
&p.ImageURL, &p.Category, &p.IsActive, &p.CreatedAt, &p.UpdatedAt,
|
||||
&p.ID, &p.Name, &p.Description, &p.Details, &p.Price, &p.SalePrice,
|
||||
&p.ImageURL, &p.Category, &catID, &p.CategorySlug, &p.IsActive, &p.CreatedAt, &p.UpdatedAt,
|
||||
)
|
||||
if catID.Valid {
|
||||
id := int(catID.Int64)
|
||||
p.CategoryID = &id
|
||||
}
|
||||
return p, err
|
||||
}
|
||||
|
||||
const productColumns = `id, name, description, details, price, image_url, category, is_active, created_at, updated_at`
|
||||
|
||||
func (r *ProductRepository) Featured(ctx context.Context, limit int) ([]models.Product, error) {
|
||||
rows, err := r.pool.Query(ctx, `
|
||||
SELECT `+productColumns+`
|
||||
FROM products
|
||||
WHERE is_active = true
|
||||
ORDER BY created_at DESC
|
||||
LIMIT $1
|
||||
`, limit)
|
||||
rows, err := r.pool.Query(ctx, productSelect+`
|
||||
WHERE p.is_active = true
|
||||
ORDER BY p.created_at DESC LIMIT $1`, limit)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -46,11 +53,18 @@ func (r *ProductRepository) Featured(ctx context.Context, limit int) ([]models.P
|
||||
}
|
||||
|
||||
func (r *ProductRepository) All(ctx context.Context) ([]models.Product, error) {
|
||||
rows, err := r.pool.Query(ctx, `
|
||||
SELECT `+productColumns+`
|
||||
FROM products
|
||||
ORDER BY created_at DESC
|
||||
`)
|
||||
rows, err := r.pool.Query(ctx, productSelect+` ORDER BY p.created_at DESC`)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
return r.collectProducts(rows)
|
||||
}
|
||||
|
||||
func (r *ProductRepository) ByCategoryID(ctx context.Context, categoryID int) ([]models.Product, error) {
|
||||
rows, err := r.pool.Query(ctx, productSelect+`
|
||||
WHERE p.is_active = true AND p.category_id = $1
|
||||
ORDER BY p.created_at DESC`, categoryID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -59,7 +73,17 @@ func (r *ProductRepository) All(ctx context.Context) ([]models.Product, error) {
|
||||
}
|
||||
|
||||
func (r *ProductRepository) GetByID(ctx context.Context, id int) (models.Product, error) {
|
||||
row := r.pool.QueryRow(ctx, `SELECT `+productColumns+` FROM products WHERE id = $1`, id)
|
||||
row := r.pool.QueryRow(ctx, productSelect+` WHERE p.id = $1`, id)
|
||||
p, err := r.scanProduct(row)
|
||||
if err != nil {
|
||||
return models.Product{}, err
|
||||
}
|
||||
p.Images, err = r.ImagesByProduct(ctx, id)
|
||||
return p, err
|
||||
}
|
||||
|
||||
func (r *ProductRepository) GetActiveByID(ctx context.Context, id int) (models.Product, error) {
|
||||
row := r.pool.QueryRow(ctx, productSelect+` WHERE p.id = $1 AND p.is_active = true`, id)
|
||||
p, err := r.scanProduct(row)
|
||||
if err != nil {
|
||||
return models.Product{}, err
|
||||
@@ -70,20 +94,20 @@ func (r *ProductRepository) GetByID(ctx context.Context, id int) (models.Product
|
||||
|
||||
func (r *ProductRepository) Create(ctx context.Context, p *models.Product) error {
|
||||
return r.pool.QueryRow(ctx, `
|
||||
INSERT INTO products (name, description, details, price, image_url, category, is_active)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7)
|
||||
INSERT INTO products (name, description, details, price, sale_price, image_url, category, category_id, is_active)
|
||||
VALUES ($1, $2, $3, $4, NULLIF($5, 0), $6, $7, $8, $9)
|
||||
RETURNING id, created_at, updated_at
|
||||
`, p.Name, p.Description, p.Details, p.Price, p.ImageURL, p.Category, p.IsActive,
|
||||
`, p.Name, p.Description, p.Details, p.Price, p.SalePrice, p.ImageURL, p.Category, p.CategoryID, p.IsActive,
|
||||
).Scan(&p.ID, &p.CreatedAt, &p.UpdatedAt)
|
||||
}
|
||||
|
||||
func (r *ProductRepository) Update(ctx context.Context, p *models.Product) error {
|
||||
tag, err := r.pool.Exec(ctx, `
|
||||
UPDATE products
|
||||
SET name = $1, description = $2, details = $3, price = $4,
|
||||
image_url = $5, category = $6, is_active = $7, updated_at = NOW()
|
||||
WHERE id = $8
|
||||
`, p.Name, p.Description, p.Details, p.Price, p.ImageURL, p.Category, p.IsActive, p.ID)
|
||||
UPDATE products SET name = $1, description = $2, details = $3, price = $4,
|
||||
sale_price = NULLIF($5, 0), image_url = $6, category = $7, category_id = $8,
|
||||
is_active = $9, updated_at = NOW()
|
||||
WHERE id = $10
|
||||
`, p.Name, p.Description, p.Details, p.Price, p.SalePrice, p.ImageURL, p.Category, p.CategoryID, p.IsActive, p.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -106,10 +130,12 @@ func (r *ProductRepository) Delete(ctx context.Context, id int) error {
|
||||
|
||||
func (r *ProductRepository) Categories(ctx context.Context) ([]models.Category, error) {
|
||||
rows, err := r.pool.Query(ctx, `
|
||||
SELECT category, COUNT(*) AS cnt
|
||||
FROM products
|
||||
GROUP BY category
|
||||
ORDER BY cnt DESC
|
||||
SELECT c.id, c.name, c.slug, c.description, c.sort_order,
|
||||
COUNT(p.id) FILTER (WHERE p.is_active = true)
|
||||
FROM categories c
|
||||
LEFT JOIN products p ON p.category_id = c.id
|
||||
GROUP BY c.id
|
||||
ORDER BY c.sort_order, c.name
|
||||
`)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -119,10 +145,9 @@ func (r *ProductRepository) Categories(ctx context.Context) ([]models.Category,
|
||||
var categories []models.Category
|
||||
for rows.Next() {
|
||||
var c models.Category
|
||||
if err := rows.Scan(&c.Name, &c.Count); err != nil {
|
||||
if err := rows.Scan(&c.ID, &c.Name, &c.Slug, &c.Description, &c.SortOrder, &c.Count); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
c.Slug = c.Name
|
||||
categories = append(categories, c)
|
||||
}
|
||||
return categories, rows.Err()
|
||||
@@ -131,8 +156,7 @@ func (r *ProductRepository) Categories(ctx context.Context) ([]models.Category,
|
||||
func (r *ProductRepository) ImagesByProduct(ctx context.Context, productID int) ([]models.ProductImage, error) {
|
||||
rows, err := r.pool.Query(ctx, `
|
||||
SELECT id, product_id, file_path, is_primary, sort_order, created_at
|
||||
FROM product_images
|
||||
WHERE product_id = $1
|
||||
FROM product_images WHERE product_id = $1
|
||||
ORDER BY is_primary DESC, sort_order ASC, id ASC
|
||||
`, productID)
|
||||
if err != nil {
|
||||
@@ -154,21 +178,17 @@ func (r *ProductRepository) ImagesByProduct(ctx context.Context, productID int)
|
||||
func (r *ProductRepository) AddImage(ctx context.Context, img *models.ProductImage) error {
|
||||
return r.pool.QueryRow(ctx, `
|
||||
INSERT INTO product_images (product_id, file_path, is_primary, sort_order)
|
||||
VALUES ($1, $2, $3, $4)
|
||||
RETURNING id, created_at
|
||||
`, img.ProductID, img.FilePath, img.IsPrimary, img.SortOrder,
|
||||
).Scan(&img.ID, &img.CreatedAt)
|
||||
VALUES ($1, $2, $3, $4) RETURNING id, created_at
|
||||
`, img.ProductID, img.FilePath, img.IsPrimary, img.SortOrder).Scan(&img.ID, &img.CreatedAt)
|
||||
}
|
||||
|
||||
func (r *ProductRepository) DeleteImage(ctx context.Context, imageID, productID int) (string, error) {
|
||||
var path string
|
||||
err := r.pool.QueryRow(ctx, `
|
||||
DELETE FROM product_images
|
||||
WHERE id = $1 AND product_id = $2
|
||||
RETURNING file_path
|
||||
DELETE FROM product_images WHERE id = $1 AND product_id = $2 RETURNING file_path
|
||||
`, imageID, productID).Scan(&path)
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
return "", fmt.Errorf("image not found")
|
||||
return "", errors.New("image not found")
|
||||
}
|
||||
return path, err
|
||||
}
|
||||
@@ -177,12 +197,17 @@ func (r *ProductRepository) collectProducts(rows pgx.Rows) ([]models.Product, er
|
||||
var products []models.Product
|
||||
for rows.Next() {
|
||||
var p models.Product
|
||||
var catID sql.NullInt64
|
||||
if err := rows.Scan(
|
||||
&p.ID, &p.Name, &p.Description, &p.Details, &p.Price,
|
||||
&p.ImageURL, &p.Category, &p.IsActive, &p.CreatedAt, &p.UpdatedAt,
|
||||
&p.ID, &p.Name, &p.Description, &p.Details, &p.Price, &p.SalePrice,
|
||||
&p.ImageURL, &p.Category, &catID, &p.CategorySlug, &p.IsActive, &p.CreatedAt, &p.UpdatedAt,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if catID.Valid {
|
||||
id := int(catID.Int64)
|
||||
p.CategoryID = &id
|
||||
}
|
||||
products = append(products, p)
|
||||
}
|
||||
return products, rows.Err()
|
||||
|
||||
Reference in New Issue
Block a user