package waitlist import ( "context" "errors" "fmt" "log/slog" "time" pbaero "github.com/flowy-live/llink/genproto/aero" "github.com/flowy-live/llink/internal/utils" "github.com/jackc/pgx/v5/pgxpool" ) type WaitlistEntry struct { Id int `db:"id"` Email string `db:"email"` Metadata map[string]string `db:"metadata"` CreatedAt time.Time `db:"created_at"` InvitedAt *time.Time `db:"invited_at"` } var ( AlreadyInWaitlistError = errors.New("already in the waitlist") EntryNotFoundError = errors.New("not found") ) type GetWaitlistFilter string const ( GetWaitlistFilterAll GetWaitlistFilter = "all" GetWaitlistFilterInvitedOnly GetWaitlistFilter = "invited-only" GetWaitlistFilterUninvitedOnly GetWaitlistFilter = "uninvited-only" ) type Service interface { // returns AlreadyInWaitlistError if already in the waitlist // any other error is a failure AddToWaitlist(ctx context.Context, email string, metadata map[string]string) error GetWaitlist(ctx context.Context, filter GetWaitlistFilter) ([]*WaitlistEntry, error) // returns error if not found GetWaitlistEntryByEmail(ctx context.Context, email string) (*WaitlistEntry, error) MarkWaitlistEntryInvited(ctx context.Context, email string) error } type serviceImpl struct { repo repository aeroSvc pbaero.PrimaryClient } func NewService(pool *pgxpool.Pool, aeroSvc pbaero.PrimaryClient) Service { return &serviceImpl{ repo: newRepository(pool), aeroSvc: aeroSvc, } } func (s *serviceImpl) AddToWaitlist(ctx context.Context, email string, metadata map[string]string) error { normalizedEmail, err := utils.NormalizeEmail(email) if err != nil { return errors.New("invalid email") } if metadata == nil { metadata = map[string]string{} } _, err = s.repo.create(ctx, normalizedEmail, metadata) if err != nil { if errors.Is(err, errAlreadyExists) { return AlreadyInWaitlistError } return err } message := fmt.Sprintf("New human added to the llink waitlist: %s", email) _, err = s.aeroSvc.ShootEmail(context.Background(), &pbaero.ShootEmailRequest{ ToEmails: []string{"team@flowylabs.ai"}, Subject: "Flowy Llink - New Waitlist Subscriber", TemplateData: &pbaero.ShootEmailRequest_GenericFlowyAdminAlertData{ GenericFlowyAdminAlertData: &pbaero.GenericFlowyAdminAlertData{ Message: message, }, }, }) if err != nil { slog.Error("failed to add to waitlist", "error", err, "email", email) } return nil } func (s *serviceImpl) GetWaitlist(ctx context.Context, filter GetWaitlistFilter) ([]*WaitlistEntry, error) { return s.repo.getAll(ctx, filter) } func (s *serviceImpl) GetWaitlistEntryByEmail(ctx context.Context, email string) (*WaitlistEntry, error) { normalizedEmail, err := utils.NormalizeEmail(email) if err != nil { return nil, errors.New("invalid email") } entry, err := s.repo.getByEmail(ctx, normalizedEmail) if err != nil { if errors.Is(err, errNotFound) { return nil, EntryNotFoundError } return nil, err } return entry, nil } func (s *serviceImpl) MarkWaitlistEntryInvited(ctx context.Context, email string) error { normalizedEmail, err := utils.NormalizeEmail(email) if err != nil { return errors.New("invalid email") } err = s.repo.markInvited(ctx, normalizedEmail) if err != nil { if errors.Is(err, errNotFound) { return EntryNotFoundError } return err } return nil }