Files
warmbly/internal/app/feature/gate.go
T
Máté Mészáros (Laptop) 41624a6f79 Analytics & Tracking
2026-01-29 05:59:04 +01:00

206 lines
6.1 KiB
Go

package feature
import (
"context"
"github.com/google/uuid"
"github.com/warmbly/warmbly/internal/errx"
"github.com/warmbly/warmbly/internal/models"
"github.com/warmbly/warmbly/internal/repository"
)
const (
// FreeTierDailyEmailLimit is the daily campaign email limit for free trial users
FreeTierDailyEmailLimit = 20
// UnlimitedEmails indicates unlimited daily emails
UnlimitedEmails = -1
)
type FeatureGateService interface {
// CanSendCampaignEmail checks if an organization can send campaign emails
CanSendCampaignEmail(ctx context.Context, orgID uuid.UUID) (bool, *errx.Error)
// CanUseWarmup checks if an organization can use the warmup feature
CanUseWarmup(ctx context.Context, orgID uuid.UUID) (bool, *errx.Error)
// CanUseUnibox checks if an organization can use the unibox feature
CanUseUnibox(ctx context.Context, orgID uuid.UUID) (bool, *errx.Error)
// GetDailyEmailLimit returns the daily email limit for an organization
// Returns -1 for unlimited, 0 for blocked
GetDailyEmailLimit(ctx context.Context, orgID uuid.UUID) (int, *errx.Error)
// GetSubscriptionStatus returns subscription info for feature checks
GetSubscriptionStatus(ctx context.Context, orgID uuid.UUID) (*SubscriptionStatus, *errx.Error)
// IsPaidOrganization checks if the organization has an active paid subscription
IsPaidOrganization(ctx context.Context, orgID uuid.UUID) (bool, *errx.Error)
}
// SubscriptionStatus contains status info for feature gating
type SubscriptionStatus struct {
HasSubscription bool `json:"has_subscription"`
IsInFreeTrial bool `json:"is_in_free_trial"`
IsFreeTrialExpired bool `json:"is_free_trial_expired"`
IsPaidSubscriber bool `json:"is_paid_subscriber"`
DailyEmailLimit int `json:"daily_email_limit"`
Plan *models.Plan `json:"plan,omitempty"`
}
type featureGateService struct {
subRepo repository.SubscriptionRepository
planRepo repository.PlanRepository
}
func NewService(subRepo repository.SubscriptionRepository, planRepo repository.PlanRepository) FeatureGateService {
return &featureGateService{
subRepo: subRepo,
planRepo: planRepo,
}
}
// CanSendCampaignEmail checks if an organization can send campaign emails
func (s *featureGateService) CanSendCampaignEmail(ctx context.Context, orgID uuid.UUID) (bool, *errx.Error) {
sub, err := s.subRepo.GetByOrganizationID(ctx, orgID)
if err != nil {
return false, errx.New(errx.Internal, "failed to get subscription")
}
// No subscription = blocked
if sub == nil {
return false, nil
}
// Active paid subscription = allowed
if sub.HasPaidSubscription() {
return true, nil
}
// In free trial = allowed (with limit)
if sub.IsInFreeTrial() {
return true, nil
}
// Trial expired, no paid subscription = blocked
return false, nil
}
// CanUseWarmup checks if an organization can use the warmup feature
func (s *featureGateService) CanUseWarmup(ctx context.Context, orgID uuid.UUID) (bool, *errx.Error) {
sub, err := s.subRepo.GetByOrganizationID(ctx, orgID)
if err != nil {
return false, errx.New(errx.Internal, "failed to get subscription")
}
if sub == nil {
return false, nil
}
// Only paid subscribers can use warmup
return sub.HasPaidSubscription(), nil
}
// CanUseUnibox checks if an organization can use the unibox feature
func (s *featureGateService) CanUseUnibox(ctx context.Context, orgID uuid.UUID) (bool, *errx.Error) {
sub, err := s.subRepo.GetByOrganizationID(ctx, orgID)
if err != nil {
return false, errx.New(errx.Internal, "failed to get subscription")
}
if sub == nil {
return false, nil
}
// Only paid subscribers can use unibox
return sub.HasPaidSubscription(), nil
}
// GetDailyEmailLimit returns the daily email limit for an organization
func (s *featureGateService) GetDailyEmailLimit(ctx context.Context, orgID uuid.UUID) (int, *errx.Error) {
sub, err := s.subRepo.GetByOrganizationID(ctx, orgID)
if err != nil {
return 0, errx.New(errx.Internal, "failed to get subscription")
}
if sub == nil {
return 0, nil // No subscription = no emails
}
// Free trial users = 20 emails/day
if sub.IsInFreeTrial() && !sub.HasPaidSubscription() {
return FreeTierDailyEmailLimit, nil
}
// Paid users = plan limit or unlimited
if sub.HasPaidSubscription() {
plan, err := s.planRepo.GetByID(ctx, sub.PlanID)
if err != nil || plan == nil {
return UnlimitedEmails, nil // Default to unlimited if plan not found
}
if plan.DailyCampaignLimit != nil {
return *plan.DailyCampaignLimit, nil
}
return UnlimitedEmails, nil // -1 = unlimited
}
// Trial expired = no emails
return 0, nil
}
// GetSubscriptionStatus returns subscription info for feature checks
func (s *featureGateService) GetSubscriptionStatus(ctx context.Context, orgID uuid.UUID) (*SubscriptionStatus, *errx.Error) {
status := &SubscriptionStatus{
HasSubscription: false,
IsInFreeTrial: false,
IsFreeTrialExpired: false,
IsPaidSubscriber: false,
DailyEmailLimit: 0,
}
sub, err := s.subRepo.GetByOrganizationID(ctx, orgID)
if err != nil {
return nil, errx.New(errx.Internal, "failed to get subscription")
}
if sub == nil {
return status, nil
}
status.HasSubscription = true
status.IsInFreeTrial = sub.IsInFreeTrial()
status.IsFreeTrialExpired = sub.IsFreeTrialExpired()
status.IsPaidSubscriber = sub.HasPaidSubscription()
// Load plan
plan, _ := s.planRepo.GetByID(ctx, sub.PlanID)
status.Plan = plan
// Calculate daily limit
if status.IsInFreeTrial && !status.IsPaidSubscriber {
status.DailyEmailLimit = FreeTierDailyEmailLimit
} else if status.IsPaidSubscriber {
if plan != nil && plan.DailyCampaignLimit != nil {
status.DailyEmailLimit = *plan.DailyCampaignLimit
} else {
status.DailyEmailLimit = UnlimitedEmails
}
}
return status, nil
}
// IsPaidOrganization checks if the organization has an active paid subscription
func (s *featureGateService) IsPaidOrganization(ctx context.Context, orgID uuid.UUID) (bool, *errx.Error) {
sub, err := s.subRepo.GetByOrganizationID(ctx, orgID)
if err != nil {
return false, errx.New(errx.Internal, "failed to get subscription")
}
if sub == nil {
return false, nil
}
return sub.HasPaidSubscription(), nil
}