157 lines
4.2 KiB
Go
157 lines
4.2 KiB
Go
package storage
|
||
|
||
import (
|
||
"context"
|
||
"errors"
|
||
"fmt"
|
||
|
||
"github.com/jackc/pgx/v5"
|
||
|
||
"dana/internal/domain"
|
||
)
|
||
|
||
var ErrNotFound = errors.New("not found")
|
||
|
||
// wrapNotFound переводит «пустой результат» pgx в ErrNotFound, прочие ошибки
|
||
// возвращает как есть.
|
||
func wrapNotFound(err error) error {
|
||
if errors.Is(err, pgx.ErrNoRows) {
|
||
return ErrNotFound
|
||
}
|
||
return err
|
||
}
|
||
|
||
const serviceColumns = `s.id, s.parent_id, s.slug, s.path, s.name, s.short_name,
|
||
s.lead, s.icon, COALESCE(m.path, ''), s.seo_title, s.seo_description, s.seo_h1`
|
||
|
||
func scanService(row pgx.Row, s *domain.Service) error {
|
||
return row.Scan(&s.ID, &s.ParentID, &s.Slug, &s.Path, &s.Name, &s.ShortName,
|
||
&s.Lead, &s.Icon, &s.Image, &s.SEO.Title, &s.SEO.Description, &s.SEO.H1)
|
||
}
|
||
|
||
// ServiceTree возвращает опубликованные услуги деревом.
|
||
func (s *Storage) ServiceTree(ctx context.Context) ([]*domain.Service, error) {
|
||
rows, err := s.pool.Query(ctx, `
|
||
SELECT `+serviceColumns+`
|
||
FROM services s
|
||
LEFT JOIN media m ON m.id = s.image_id
|
||
WHERE s.published AND s.deleted_at IS NULL
|
||
ORDER BY s.sort, s.name`)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
defer rows.Close()
|
||
|
||
byID := map[int64]*domain.Service{}
|
||
var all []*domain.Service
|
||
for rows.Next() {
|
||
svc := &domain.Service{}
|
||
if err := scanService(rows, svc); err != nil {
|
||
return nil, err
|
||
}
|
||
byID[svc.ID] = svc
|
||
all = append(all, svc)
|
||
}
|
||
if err := rows.Err(); err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
var roots []*domain.Service
|
||
for _, svc := range all {
|
||
if svc.ParentID == nil {
|
||
roots = append(roots, svc)
|
||
continue
|
||
}
|
||
if parent, ok := byID[*svc.ParentID]; ok {
|
||
parent.Children = append(parent.Children, svc)
|
||
}
|
||
}
|
||
return roots, nil
|
||
}
|
||
|
||
// ServiceByPath возвращает карточку услуги с ценами и врачами.
|
||
func (s *Storage) ServiceByPath(ctx context.Context, path string) (*domain.Service, error) {
|
||
svc := &domain.Service{}
|
||
row := s.pool.QueryRow(ctx, `
|
||
SELECT `+serviceColumns+`, s.blocks
|
||
FROM services s
|
||
LEFT JOIN media m ON m.id = s.image_id
|
||
WHERE s.path = $1 AND s.published AND s.deleted_at IS NULL`, path)
|
||
|
||
err := row.Scan(&svc.ID, &svc.ParentID, &svc.Slug, &svc.Path, &svc.Name, &svc.ShortName,
|
||
&svc.Lead, &svc.Icon, &svc.Image, &svc.SEO.Title, &svc.SEO.Description, &svc.SEO.H1,
|
||
&svc.Blocks)
|
||
if errors.Is(err, pgx.ErrNoRows) {
|
||
return nil, ErrNotFound
|
||
}
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
if svc.Prices, svc.PriceFrom, err = s.pricesForService(ctx, svc.ID); err != nil {
|
||
return nil, fmt.Errorf("prices: %w", err)
|
||
}
|
||
if svc.Doctors, err = s.DoctorsByService(ctx, svc.ID); err != nil {
|
||
return nil, fmt.Errorf("doctors: %w", err)
|
||
}
|
||
|
||
children, err := s.childServices(ctx, svc.ID)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
svc.Children = children
|
||
return svc, nil
|
||
}
|
||
|
||
func (s *Storage) childServices(ctx context.Context, parentID int64) ([]*domain.Service, error) {
|
||
rows, err := s.pool.Query(ctx, `
|
||
SELECT `+serviceColumns+`
|
||
FROM services s
|
||
LEFT JOIN media m ON m.id = s.image_id
|
||
WHERE s.parent_id = $1 AND s.published AND s.deleted_at IS NULL
|
||
ORDER BY s.sort, s.name`, parentID)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
defer rows.Close()
|
||
|
||
var out []*domain.Service
|
||
for rows.Next() {
|
||
svc := &domain.Service{}
|
||
if err := scanService(rows, svc); err != nil {
|
||
return nil, err
|
||
}
|
||
out = append(out, svc)
|
||
}
|
||
return out, rows.Err()
|
||
}
|
||
|
||
// pricesForService отдаёт позиции прайса услуги и минимальную цену для «от N ₽».
|
||
func (s *Storage) pricesForService(ctx context.Context, serviceID int64) ([]domain.PriceItem, *float64, error) {
|
||
rows, err := s.pool.Query(ctx, `
|
||
SELECT id, name, price, unit, price_from, exclude_from_min
|
||
FROM price_items
|
||
WHERE service_id = $1 AND published
|
||
ORDER BY sort, name`, serviceID)
|
||
if err != nil {
|
||
return nil, nil, err
|
||
}
|
||
defer rows.Close()
|
||
|
||
var items []domain.PriceItem
|
||
var min *float64
|
||
for rows.Next() {
|
||
var it domain.PriceItem
|
||
var excluded bool
|
||
if err := rows.Scan(&it.ID, &it.Name, &it.Price, &it.Unit, &it.From, &excluded); err != nil {
|
||
return nil, nil, err
|
||
}
|
||
items = append(items, it)
|
||
if !excluded && (min == nil || it.Price < *min) {
|
||
v := it.Price
|
||
min = &v
|
||
}
|
||
}
|
||
return items, min, rows.Err()
|
||
}
|