mirror of
https://github.com/warmbly/warmbly.git
synced 2026-08-20 08:01:26 +00:00
206 lines
6.1 KiB
Go
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
|
|
}
|