package clans import ( "context" "errors" "fmt" "strings" "time" "github.com/jackc/pgx/v5/pgconn" ) // Service composes validation on top of PgStore. type Service struct { pg *PgStore now func() time.Time } // NewService wires a service. func NewService(pg *PgStore) *Service { return &Service{pg: pg, now: time.Now} } // CreateInput is what the HTTP handler hands to the service. type CreateInput struct { ID string Tag string Name string AvatarURL string } // Create validates and inserts a clan. func (s *Service) Create(ctx context.Context, in CreateInput) (Clan, error) { tag := strings.ToUpper(strings.TrimSpace(in.Tag)) if !IsValidTag(tag) { return Clan{}, fmt.Errorf("%w: tag must be 3 uppercase ASCII letters", ErrInvalidInput) } name := strings.TrimSpace(in.Name) if name == "" { return Clan{}, fmt.Errorf("%w: name required", ErrInvalidInput) } c := Clan{ ID: in.ID, Tag: tag, Name: name, AvatarURL: in.AvatarURL, } if c.ID == "" { c.ID = fmt.Sprintf("clan-%d-%d", s.now().UnixMilli(), time.Now().UnixNano()%1000) } if err := s.pg.Create(ctx, c); err != nil { var pgErr *pgconn.PgError if errors.As(err, &pgErr) && pgErr.Code == "23505" { return Clan{}, fmt.Errorf("%w: %s", ErrAlreadyExists, tag) } return Clan{}, err } return s.pg.Get(ctx, c.ID) } // Get loads a clan by id. func (s *Service) Get(ctx context.Context, id string) (Clan, error) { return s.pg.Get(ctx, id) } // GetByTag loads a clan by tag. func (s *Service) GetByTag(ctx context.Context, tag string) (Clan, error) { tag = strings.ToUpper(strings.TrimSpace(tag)) return s.pg.GetByTag(ctx, tag) } // List returns clans with limit/offset. func (s *Service) List(ctx context.Context, limit, offset int) ([]Clan, error) { return s.pg.List(ctx, limit, offset) } // UpdateInput is the patch payload. type UpdateInput struct { Name string AvatarURL string } // Update applies a partial patch. func (s *Service) Update(ctx context.Context, id string, in UpdateInput) (Clan, error) { cur, err := s.pg.Get(ctx, id) if err != nil { return Clan{}, err } if in.Name != "" { cur.Name = in.Name } if in.AvatarURL != "" { cur.AvatarURL = in.AvatarURL } if err := s.pg.Update(ctx, cur); err != nil { return Clan{}, err } return s.pg.Get(ctx, id) } // Delete removes a clan. func (s *Service) Delete(ctx context.Context, id string) error { return s.pg.Delete(ctx, id) }