package invite import ( "context" "crypto/rand" "log/slog" "math/big" "strings" "time" ) type Service struct { r *Repository codeGroupSize int codeNumGroups int } func NewService(r *Repository, codeGroupSize, codeNumGroups int) (*Service, error) { if codeGroupSize == 0 { codeGroupSize = 5 } if codeNumGroups == 0 { codeNumGroups = 5 } return &Service{r: r, codeGroupSize: codeGroupSize, codeNumGroups: codeNumGroups}, nil } func (s *Service) generateCode() (string, error) { const alphabet = "23456789ABCDEFGHJKMNPQRSTVWXYZ" totalChars := s.codeGroupSize * s.codeNumGroups alphabetLen := big.NewInt(int64(len(alphabet))) var builder strings.Builder builder.Grow(totalChars + (s.codeNumGroups - 1)) for i := range totalChars { if i > 0 && i%s.codeGroupSize == 0 { builder.WriteByte('-') } randIndex, err := rand.Int(rand.Reader, alphabetLen) if err != nil { slog.Error("generate invite code", slog.String("op", "service.generateCode"), slog.Any("error", err)) return "", ErrInternal } builder.WriteByte(alphabet[randIndex.Int64()]) } return builder.String(), nil } func (s *Service) CreateInvite(ctx context.Context, quota int, expiresAt time.Time) (*Invite, error) { token, err := s.generateCode() if err != nil { return nil, err } return s.r.CreateInvite(ctx, token, quota, expiresAt) } func (s *Service) InviteByToken(ctx context.Context, token string) (*Invite, error) { return s.r.InviteByToken(ctx, token) } func (s *Service) CountInvites(ctx context.Context) (int64, error) { return s.r.CountInvites(ctx) } func (s *Service) Invites(ctx context.Context, limit, offset int64) ([]*Invite, error) { return s.r.Invites(ctx, limit, offset) } func (s *Service) UseInvite(ctx context.Context, token string) (*Invite, error) { tx, err := s.r.Db.BeginTx(ctx, nil) if err != nil { slog.Error("begin transaction", slog.String("op", "service.UseInvite"), slog.String("token", token), slog.Any("error", err)) return nil, ErrInternal } defer tx.Rollback() txRepo := s.r.WithTx(tx) invite, err := txRepo.InviteByToken(ctx, token) if err != nil { return nil, err } if !invite.IsValid() { return nil, ErrInvalid } value := *invite cloned := &value cloned.Uses += 1 if cloned.Uses < cloned.Quota { err := txRepo.UpdateInvite(ctx, cloned) if err != nil { return nil, err } } else { err := txRepo.DeleteInvite(ctx, cloned.Token) if err != nil { return nil, err } } tx.Commit() return cloned, nil } func (s *Service) DeleteInvite(ctx context.Context, invite *Invite) error { return s.r.DeleteInvite(ctx, invite.Token) } func (s *Service) CleanupInvites(ctx context.Context) error { return s.r.CleanupInvites(ctx) }