package service import ( "fmt" "spend-sparrow/db" "spend-sparrow/log" "spend-sparrow/types" "github.com/google/uuid" "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus/promauto" ) var ( treasureChestMetric = promauto.NewCounterVec( prometheus.CounterOpts{ Name: "spendsparrow_treasurechest_total", Help: "The total of treasurechest operations", }, []string{"operation"}, ) ) type TreasureChest interface { Add(user *types.User, parentId, name string) (*types.TreasureChest, error) Update(user *types.User, id, parentId, name string) (*types.TreasureChest, error) Get(user *types.User, id string) (*types.TreasureChest, error) GetAll(user *types.User) ([]*types.TreasureChest, error) Delete(user *types.User, id string) error } type TreasureChestImpl struct { db db.TreasureChest clock Clock random Random settings *types.Settings } func NewTreasureChest(db db.TreasureChest, random Random, clock Clock, settings *types.Settings) TreasureChest { return TreasureChestImpl{ db: db, clock: clock, random: random, settings: settings, } } func (s TreasureChestImpl) Add(user *types.User, parentId, name string) (*types.TreasureChest, error) { treasureChestMetric.WithLabelValues("add").Inc() if user == nil { return nil, ErrUnauthorized } newId, err := s.random.UUID() if err != nil { return nil, types.ErrInternal } err = validateString(name, "name") if err != nil { return nil, err } parentUuid := uuid.Nil if parentId != "" { parent, err := s.Get(user, parentId) if err != nil { return nil, err } if parent.ParentId != uuid.Nil { return nil, fmt.Errorf("only a depth of 1 allowed: %w", ErrBadRequest) } parentUuid = parent.Id } treasureChest := &types.TreasureChest{ Id: newId, ParentId: parentUuid, UserId: user.Id, Name: name, CurrentBalance: 0, CreatedAt: s.clock.Now(), CreatedBy: user.Id, UpdatedAt: nil, UpdatedBy: nil, } err = s.db.Insert(user.Id, treasureChest) if err != nil { return nil, types.ErrInternal } savedtreasureChest, err := s.db.Get(user.Id, newId) if err != nil { log.Error("treasureChest %v not found after insert: %v", newId, err) return nil, types.ErrInternal } return savedtreasureChest, nil } func (s TreasureChestImpl) Update(user *types.User, idStr, parentId, name string) (*types.TreasureChest, error) { treasureChestMetric.WithLabelValues("update").Inc() if user == nil { return nil, ErrUnauthorized } err := validateString(name, "name") if err != nil { return nil, err } id, err := uuid.Parse(idStr) if err != nil { log.Error("treasureChest update: %v", err) return nil, fmt.Errorf("could not parse Id: %w", ErrBadRequest) } treasureChest, err := s.db.Get(user.Id, id) if err != nil { if err == db.ErrNotFound { return nil, fmt.Errorf("treasureChest %v not found: %w", idStr, ErrBadRequest) } return nil, types.ErrInternal } parentUuid := uuid.Nil if parentId != "" { parent, err := s.Get(user, parentId) if err != nil { return nil, err } if parent.ParentId != uuid.Nil { return nil, fmt.Errorf("only a depth of 1 allowed: %w", ErrBadRequest) } children, err := s.db.GetAllByParentId(user.Id, treasureChest.Id) if err != nil { return nil, err } if len(children) > 0 { return nil, fmt.Errorf("only a depth of 1 allowed: %w", ErrBadRequest) } parentUuid = parent.Id } timestamp := s.clock.Now() treasureChest.Name = name treasureChest.ParentId = parentUuid treasureChest.UpdatedAt = ×tamp treasureChest.UpdatedBy = &user.Id err = s.db.Update(user.Id, treasureChest) if err != nil { return nil, types.ErrInternal } return treasureChest, nil } func (s TreasureChestImpl) Get(user *types.User, id string) (*types.TreasureChest, error) { treasureChestMetric.WithLabelValues("get").Inc() if user == nil { return nil, ErrUnauthorized } uuid, err := uuid.Parse(id) if err != nil { log.Error("treasureChest get: %v", err) return nil, fmt.Errorf("could not parse Id: %w", ErrBadRequest) } treasureChest, err := s.db.Get(user.Id, uuid) if err != nil { if err == db.ErrNotFound { return nil, fmt.Errorf("treasureChest %v not found: %w", id, ErrBadRequest) } return nil, types.ErrInternal } return treasureChest, nil } func (s TreasureChestImpl) GetAll(user *types.User) ([]*types.TreasureChest, error) { treasureChestMetric.WithLabelValues("get_all").Inc() if user == nil { return nil, ErrUnauthorized } treasureChests, err := s.db.GetAll(user.Id) if err != nil { return nil, types.ErrInternal } return sortTree(treasureChests), nil } func (s TreasureChestImpl) Delete(user *types.User, idStr string) error { treasureChestMetric.WithLabelValues("delete").Inc() if user == nil { return ErrUnauthorized } id, err := uuid.Parse(idStr) if err != nil { log.Error("treasureChest delete: %v", err) return fmt.Errorf("could not parse Id: %w", ErrBadRequest) } treasureChest, err := s.db.Get(user.Id, id) if err != nil { if err == db.ErrNotFound { return fmt.Errorf("treasureChest %v not found: %w", idStr, ErrBadRequest) } return types.ErrInternal } if treasureChest.UserId != user.Id { return types.ErrUnauthorized } children, err := s.db.GetAllByParentId(user.Id, treasureChest.Id) if err != nil { log.Error("treasureChest delete: %v", err) return types.ErrInternal } if len(children) > 0 { return fmt.Errorf("TreasureChest %v has children: %w", idStr, ErrBadRequest) } err = s.db.Delete(user.Id, treasureChest.Id) if err != nil { return types.ErrInternal } return nil } func sortTree(nodes []*types.TreasureChest) []*types.TreasureChest { var ( roots []*types.TreasureChest result []*types.TreasureChest ) children := make(map[uuid.UUID][]*types.TreasureChest) for _, node := range nodes { if node.ParentId == uuid.Nil { roots = append(roots, node) } else { children[node.ParentId] = append(children[node.ParentId], node) } } for _, root := range roots { result = append(result, root) result = append(result, children[root.Id]...) } return result }