Files
Arjun Patel 563c91e7d5 infra: add logging wrapping
Resolves issues with gcp cloud logging quirks such as field names
2026-05-27 15:49:27 -07:00

131 lines
3.4 KiB
Go

package waitlist
import (
"context"
"errors"
"fmt"
"time"
"github.com/flowy-live/llink/internal/utils/flog"
"github.com/jackc/pgx/v5/pgxpool"
pbaero "github.com/flowy-live/llink/genproto/aero"
"github.com/flowy-live/llink/internal/utils"
)
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 {
// AddToWaitlist returns AlreadyInWaitlistError if the email is already present.
AddToWaitlist(ctx context.Context, email string, metadata map[string]string) error
GetWaitlist(ctx context.Context, filter GetWaitlistFilter) ([]*WaitlistEntry, error)
// GetWaitlistEntryByEmail returns EntryNotFoundError if missing.
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 {
flog.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
}