package campaigns import ( "net/http" "sync" "time" "github.com/descrybe/descrybe-v2/apps/api/internal/aiprompts" "github.com/descrybe/descrybe-v2/apps/api/internal/aiprovider" "github.com/descrybe/descrybe-v2/apps/api/internal/billing" "github.com/descrybe/descrybe-v2/apps/api/internal/email" "github.com/descrybe/descrybe-v2/apps/api/internal/processing" "github.com/jackc/pgx/v5/pgxpool" ) // Service is the email campaigns API surface (CRUD + generate + schedule/send). // Tenant sending goes through email.Service (verified provider, rate limits, unsub). type Service struct { Pool *pgxpool.Pool Billing *billing.Service Email *email.Service Completer processing.Completer AI *aiprovider.Service Prompts *aiprompts.Service WebOrigin string PublicAPIURL string // TokenSigningSecret signs public brand-logo URLs for email embeds. TokenSigningSecret string HTTP *http.Client genMu sync.Mutex genHit map[string][]time.Time sendMu sync.Mutex sendHit map[string][]time.Time } func NewService(pool *pgxpool.Pool, billingSvc *billing.Service, emailSvc *email.Service) *Service { return &Service{ Pool: pool, Billing: billingSvc, Email: emailSvc, HTTP: &http.Client{Timeout: 30 * time.Second}, genHit: make(map[string][]time.Time), sendHit: make(map[string][]time.Time), } } const ( generateRPM = 10 sendRPM = 20 ) func (s *Service) allowGenerate(companyID string) bool { return allowWindow(&s.genMu, s.genHit, companyID, generateRPM, time.Minute) } func (s *Service) allowSend(companyID string) bool { return allowWindow(&s.sendMu, s.sendHit, companyID, sendRPM, time.Minute) } func allowWindow(mu *sync.Mutex, hits map[string][]time.Time, key string, limit int, window time.Duration) bool { now := time.Now() cutoff := now.Add(-window) mu.Lock() defer mu.Unlock() ts := hits[key] kept := ts[:0] for _, t := range ts { if t.After(cutoff) { kept = append(kept, t) } } if len(kept) >= limit { hits[key] = kept return false } hits[key] = append(kept, now) return true }