251 рядки
7.0 KiB
Go
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
|
|
}
|
|
}
|