dana-medical-site/internal/storage/services.go
2026-10-14 13:33:13 +00:00

157 lines
4.2 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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()
}