Файли
2026-11-07 20:55:19 +00:00

251 рядки
7.0 KiB
Go

package service
import (
"context"
"fmt"
"math"
"strings"
"unicode/utf8"
"git.entityseven.com/rilaxik/pllist/internal/domain"
)
const (
ItemPageSize = 10
maxItemTitleLength = 200
maxItemNoteLength = 1000
)
type ItemRepository interface {
Create(ctx context.Context, listID, createdByID int64, title string) (domain.Item, error)
Get(ctx context.Context, id int64) (domain.Item, error)
ListByList(ctx context.Context, listID int64, limit, offset int) ([]domain.Item, error)
CountByList(ctx context.Context, listID int64) (int64, error)
UpdateTitle(ctx context.Context, id int64, title string) error
UpdateNote(ctx context.Context, id int64, note string) error
UpdateQuantity(ctx context.Context, id int64, quantity float64) error
UpdateUrgency(ctx context.Context, id int64, urgency domain.Urgency) error
Delete(ctx context.Context, id int64) error
SetDone(ctx context.Context, id int64) error
SetOpen(ctx context.Context, id int64) error
Reserve(ctx context.Context, id, userID int64) error
Release(ctx context.Context, id int64) error
}
type ItemService struct {
items ItemRepository
access *AccessService
}
func NewItemService(items ItemRepository, access *AccessService) *ItemService {
return &ItemService{items: items, access: access}
}
type ItemPage struct {
Access domain.Access
Items []domain.Item
Page int
Total int
}
func (p ItemPage) PageCount() int {
if p.Total == 0 {
return 1
}
return (p.Total + ItemPageSize - 1) / ItemPageSize
}
func (p ItemPage) HasPrev() bool { return p.Page > 1 }
func (p ItemPage) HasNext() bool { return p.Page < p.PageCount() }
type ItemAccess struct {
Item domain.Item
Access domain.Access
}
func (s *ItemService) List(
ctx context.Context, userID, listID int64, page int,
) (ItemPage, error) {
access, err := s.access.Authorize(ctx, userID, listID, domain.ActionViewItems)
if err != nil {
return ItemPage{}, err
}
total, err := s.items.CountByList(ctx, listID)
if err != nil {
return ItemPage{}, err
}
if page < 1 {
page = 1
}
pageCount := (int(total) + ItemPageSize - 1) / ItemPageSize
if pageCount < 1 {
pageCount = 1
}
if page > pageCount {
page = pageCount
}
items, err := s.items.ListByList(ctx, listID, ItemPageSize, (page-1)*ItemPageSize)
if err != nil {
return ItemPage{}, err
}
return ItemPage{Access: access, Items: items, Page: page, Total: int(total)}, nil
}
func (s *ItemService) Get(ctx context.Context, userID, itemID int64) (ItemAccess, error) {
return s.Authorize(ctx, userID, itemID, domain.ActionViewItems)
}
func (s *ItemService) Authorize(
ctx context.Context, userID, itemID int64, action domain.Action,
) (ItemAccess, error) {
item, err := s.items.Get(ctx, itemID)
if err != nil {
return ItemAccess{}, err
}
access, err := s.access.Authorize(ctx, userID, item.ListID, action)
if err != nil {
return ItemAccess{}, err
}
return ItemAccess{Item: item, Access: access}, nil
}
func (s *ItemService) Create(
ctx context.Context, userID, listID int64, title string,
) (domain.Item, error) {
if _, err := s.access.Authorize(ctx, userID, listID, domain.ActionAddItem); err != nil {
return domain.Item{}, err
}
title, err := validateItemTitle(title)
if err != nil {
return domain.Item{}, err
}
return s.items.Create(ctx, listID, userID, title)
}
func (s *ItemService) Rename(ctx context.Context, userID, itemID int64, title string) error {
if _, err := s.Authorize(ctx, userID, itemID, domain.ActionEditItem); err != nil {
return err
}
title, err := validateItemTitle(title)
if err != nil {
return err
}
return s.items.UpdateTitle(ctx, itemID, title)
}
func (s *ItemService) SetNote(ctx context.Context, userID, itemID int64, note string) error {
if _, err := s.Authorize(ctx, userID, itemID, domain.ActionEditItem); err != nil {
return err
}
note, err := validateItemNote(note)
if err != nil {
return err
}
return s.items.UpdateNote(ctx, itemID, note)
}
func (s *ItemService) SetQuantity(ctx context.Context, userID, itemID int64, quantity float64) error {
if _, err := s.Authorize(ctx, userID, itemID, domain.ActionEditItem); err != nil {
return err
}
quantity, err := normalizeItemQuantity(quantity)
if err != nil {
return err
}
return s.items.UpdateQuantity(ctx, itemID, quantity)
}
func (s *ItemService) SetUrgency(
ctx context.Context, userID, itemID int64, urgency domain.Urgency,
) error {
if _, err := s.Authorize(ctx, userID, itemID, domain.ActionEditItem); err != nil {
return err
}
if !validUrgency(urgency) {
return domain.NewValidationError("Invalid urgency.")
}
return s.items.UpdateUrgency(ctx, itemID, urgency)
}
func (s *ItemService) Delete(ctx context.Context, userID, itemID int64) error {
if _, err := s.Authorize(ctx, userID, itemID, domain.ActionDeleteItem); err != nil {
return err
}
return s.items.Delete(ctx, itemID)
}
func (s *ItemService) SetDone(ctx context.Context, userID, itemID int64) error {
if _, err := s.Authorize(ctx, userID, itemID, domain.ActionChangeItemState); err != nil {
return err
}
return s.items.SetDone(ctx, itemID)
}
func (s *ItemService) Reopen(ctx context.Context, userID, itemID int64) error {
if _, err := s.Authorize(ctx, userID, itemID, domain.ActionChangeItemState); err != nil {
return err
}
return s.items.SetOpen(ctx, itemID)
}
func (s *ItemService) Reserve(ctx context.Context, userID, itemID int64) error {
if _, err := s.Authorize(ctx, userID, itemID, domain.ActionReserveItem); err != nil {
return err
}
return s.items.Reserve(ctx, itemID, userID)
}
func (s *ItemService) Release(ctx context.Context, userID, itemID int64) error {
if _, err := s.Authorize(ctx, userID, itemID, domain.ActionReserveItem); err != nil {
return err
}
return s.items.Release(ctx, itemID)
}
func validateItemTitle(title string) (string, error) {
title = strings.TrimSpace(title)
if title == "" || utf8.RuneCountInString(title) > maxItemTitleLength {
return "", domain.NewValidationError(
fmt.Sprintf("Item title must be 1-%d characters.", maxItemTitleLength),
)
}
return title, nil
}
func validateItemNote(note string) (string, error) {
note = strings.TrimSpace(note)
if utf8.RuneCountInString(note) > maxItemNoteLength {
return "", domain.NewValidationError(
fmt.Sprintf("Note must be at most %d characters.", maxItemNoteLength),
)
}
return note, nil
}
// Item quantity is stored as numeric(12,3), so keep the same precision in the
// domain instead of letting the database round silently.
func normalizeItemQuantity(quantity float64) (float64, error) {
if math.IsNaN(quantity) || math.IsInf(quantity, 0) || quantity <= 0 {
return 0, domain.NewValidationError("Quantity must be a positive number.")
}
const scale = 1000
rounded := math.Round(quantity*scale) / scale
if math.Abs(quantity-rounded) > 1e-9 {
return 0, domain.NewValidationError("Quantity can have at most 3 decimal places.")
}
return rounded, nil
}
func validUrgency(urgency domain.Urgency) bool {
switch urgency {
case domain.UrgencyLow, domain.UrgencyNormal, domain.UrgencyHigh:
return true
default:
return false
}
}