mirror of
https://github.com/warmbly/warmbly.git
synced 2026-08-19 16:01:16 +00:00
250 lines
8.6 KiB
Go
250 lines
8.6 KiB
Go
package models
|
|
|
|
import (
|
|
"math"
|
|
"time"
|
|
|
|
"github.com/google/uuid"
|
|
)
|
|
|
|
type Duration string
|
|
|
|
const (
|
|
DurationMonth Duration = "month"
|
|
DurationYear Duration = "year"
|
|
)
|
|
|
|
type SubscriptionStatus string
|
|
|
|
const (
|
|
SubscriptionStatusTrialing SubscriptionStatus = "trialing"
|
|
SubscriptionStatusActive SubscriptionStatus = "active"
|
|
SubscriptionStatusPastDue SubscriptionStatus = "past_due"
|
|
SubscriptionStatusCanceled SubscriptionStatus = "canceled"
|
|
SubscriptionStatusUnpaid SubscriptionStatus = "unpaid"
|
|
SubscriptionStatusIncomplete SubscriptionStatus = "incomplete"
|
|
SubscriptionStatusIncompleteExpired SubscriptionStatus = "incomplete_expired"
|
|
SubscriptionStatusPaused SubscriptionStatus = "paused"
|
|
)
|
|
|
|
// IsActive returns true if the subscription allows access
|
|
func (s SubscriptionStatus) IsActive() bool {
|
|
return s == SubscriptionStatusActive || s == SubscriptionStatusTrialing
|
|
}
|
|
|
|
type Plan struct {
|
|
ID uuid.UUID `json:"id"`
|
|
Name *string `json:"name,omitempty"`
|
|
MaxContacts uint `json:"max_contacts"`
|
|
DailyEmails uint `json:"daily_emails"`
|
|
AIGeneration bool `json:"ai_generation"`
|
|
AccountLimit uint `json:"account_limit"`
|
|
Price float32 `json:"price"`
|
|
DiscountedPrice float32 `json:"discounted_price"`
|
|
Duration Duration `json:"duration"`
|
|
Savings uint8 `json:"savings"`
|
|
Public bool `json:"public"`
|
|
|
|
// Stripe integration
|
|
StripePriceID *string `json:"stripe_price_id,omitempty"`
|
|
StripePriceIDYearly *string `json:"stripe_price_id_yearly,omitempty"`
|
|
StripeProductID *string `json:"stripe_product_id,omitempty"`
|
|
|
|
// Worker tier settings
|
|
DedicatedWorkers int `json:"dedicated_workers"`
|
|
DailyCampaignLimit *int `json:"daily_campaign_limit,omitempty"`
|
|
|
|
// Organization limits
|
|
MaxCampaigns *int `json:"max_campaigns,omitempty"`
|
|
MaxActiveCampaigns *int `json:"max_active_campaigns,omitempty"`
|
|
MaxTeamMembers *int `json:"max_team_members,omitempty"`
|
|
MaxEmailAccounts *int `json:"max_email_accounts,omitempty"`
|
|
|
|
// AI writing-assistant monthly credit grant for this plan.
|
|
MonthlyCredits int `json:"monthly_credits"`
|
|
|
|
// Referral reward: the percentage of this plan's first-month-equivalent
|
|
// price a referrer earns when an invitee converts to it (100 = a full
|
|
// month-equivalent). Defaults to 100 for every plan.
|
|
ReferralRewardPercent int `json:"referral_reward_percent"`
|
|
|
|
UpdatedAt time.Time `json:"updated_at"`
|
|
CreatedAt time.Time `json:"created_at"`
|
|
}
|
|
|
|
// MonthlyPriceCents returns this plan's price normalized to a single month, in
|
|
// integer cents. Yearly plans are divided by 12 so a referral reward for an
|
|
// annual conversion is one month-equivalent (e.g. Pro Annual $990/yr -> ~$99),
|
|
// not the full annual invoice.
|
|
func (p *Plan) MonthlyPriceCents() int64 {
|
|
price := float64(p.Price)
|
|
if p.Duration == DurationYear {
|
|
price = price / 12
|
|
}
|
|
return int64(math.Round(price * 100))
|
|
}
|
|
|
|
// ReferralRewardCents returns the credit a referrer earns for converting an
|
|
// invitee onto this plan: the month-equivalent price scaled by the plan's
|
|
// referral_reward_percent.
|
|
func (p *Plan) ReferralRewardCents() int64 {
|
|
pct := p.ReferralRewardPercent
|
|
if pct <= 0 {
|
|
return 0
|
|
}
|
|
return int64(math.Round(float64(p.MonthlyPriceCents()) * float64(pct) / 100))
|
|
}
|
|
|
|
type OfferOption struct {
|
|
Title string `json:"title"`
|
|
Plan string `json:"plan"`
|
|
}
|
|
|
|
type Offer struct {
|
|
ID string `json:"id"`
|
|
Title string `json:"title"`
|
|
Description string `json:"description"`
|
|
Options []OfferOption `json:"options"`
|
|
|
|
UpdatedAt time.Time `json:"updated_at"`
|
|
CreatedAt time.Time `json:"created_at"`
|
|
}
|
|
|
|
type Subscription struct {
|
|
ID uuid.UUID `json:"id"`
|
|
UserID uuid.UUID `json:"user_id"`
|
|
OrganizationID uuid.UUID `json:"organization_id"`
|
|
PlanID uuid.UUID `json:"plan_id"`
|
|
|
|
// Stripe identifiers
|
|
StripeCustomerID string `json:"stripe_customer_id"`
|
|
StripeSubscriptionID *string `json:"stripe_subscription_id,omitempty"`
|
|
StripePriceID *string `json:"stripe_price_id,omitempty"`
|
|
|
|
// Subscription state
|
|
Status SubscriptionStatus `json:"status"`
|
|
|
|
// Billing period
|
|
CurrentPeriodStart *time.Time `json:"current_period_start,omitempty"`
|
|
CurrentPeriodEnd *time.Time `json:"current_period_end,omitempty"`
|
|
CancelAtPeriodEnd bool `json:"cancel_at_period_end"`
|
|
CanceledAt *time.Time `json:"canceled_at,omitempty"`
|
|
|
|
// Stripe trial info
|
|
TrialStart *time.Time `json:"trial_start,omitempty"`
|
|
TrialEnd *time.Time `json:"trial_end,omitempty"`
|
|
|
|
// Free trial info (internal, not Stripe)
|
|
FreeTrialStartedAt *time.Time `json:"free_trial_started_at,omitempty"`
|
|
FreeTrialEndsAt *time.Time `json:"free_trial_ends_at,omitempty"`
|
|
|
|
// Enterprise flag
|
|
IsEnterprise bool `json:"is_enterprise"`
|
|
|
|
// Joined data
|
|
Plan *Plan `json:"plan,omitempty"`
|
|
|
|
// User info (populated by joins)
|
|
UserEmail *string `json:"user_email,omitempty"`
|
|
|
|
UpdatedAt time.Time `json:"updated_at"`
|
|
CreatedAt time.Time `json:"created_at"`
|
|
}
|
|
|
|
// IsInFreeTrial returns true if the user is currently in their free trial period
|
|
func (s *Subscription) IsInFreeTrial() bool {
|
|
if s.FreeTrialEndsAt == nil {
|
|
return false
|
|
}
|
|
return time.Now().Before(*s.FreeTrialEndsAt)
|
|
}
|
|
|
|
// IsFreeTrialExpired returns true if the free trial has expired
|
|
func (s *Subscription) IsFreeTrialExpired() bool {
|
|
if s.FreeTrialEndsAt == nil {
|
|
return false
|
|
}
|
|
return time.Now().After(*s.FreeTrialEndsAt)
|
|
}
|
|
|
|
// HasPaidSubscription returns true if user has an active paid Stripe subscription
|
|
func (s *Subscription) HasPaidSubscription() bool {
|
|
return s.StripeSubscriptionID != nil && s.Status.IsActive()
|
|
}
|
|
|
|
// CanSendEmails returns true if user can send campaign emails
|
|
func (s *Subscription) CanSendEmails() bool {
|
|
// Active paid subscription
|
|
if s.HasPaidSubscription() {
|
|
return true
|
|
}
|
|
// In free trial
|
|
if s.IsInFreeTrial() {
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
// CanUseWarmup returns true if user can use warmup feature.
|
|
// Free-trial orgs get warmup access during their 14-day window so they can
|
|
// try the product; once the trial expires they need a paid subscription.
|
|
func (s *Subscription) CanUseWarmup() bool {
|
|
return s.HasPaidSubscription() || s.IsInFreeTrial()
|
|
}
|
|
|
|
// CanUseUnibox returns true if user can use unibox feature.
|
|
// Same trial allowance as warmup so a free-trial user can interact with
|
|
// their connected mailbox while evaluating Warmbly.
|
|
func (s *Subscription) CanUseUnibox() bool {
|
|
return s.HasPaidSubscription() || s.IsInFreeTrial()
|
|
}
|
|
|
|
// FreeTrialInboxLimit caps how many email accounts a free-trial org may
|
|
// connect. Paid orgs have no inbox-count cap (their plan governs sending,
|
|
// not connections). Free-trial users get up to two inboxes so they can
|
|
// evaluate the unified inbox across more than one mailbox without seeding
|
|
// the warmup pool with throwaway accounts.
|
|
const FreeTrialInboxLimit = 2
|
|
|
|
// SubscriptionWithLimits includes rate limits for the subscription
|
|
type SubscriptionWithLimits struct {
|
|
Subscription
|
|
RateLimits *RealtimeRateLimits `json:"rate_limits,omitempty"`
|
|
}
|
|
|
|
// RealtimeRateLimits contains WebSocket-specific rate limits
|
|
type RealtimeRateLimits struct {
|
|
LimitWSMessagePM int `json:"limit_ws_message_pm"`
|
|
LimitWSJoinPM int `json:"limit_ws_join_pm"`
|
|
LimitWSEventPM int `json:"limit_ws_event_pm"`
|
|
MaxConnections int `json:"max_connections"`
|
|
}
|
|
|
|
// CreateSubscriptionParams for creating a new subscription
|
|
type CreateSubscriptionParams struct {
|
|
UserID uuid.UUID `json:"user_id"`
|
|
PlanID uuid.UUID `json:"plan_id"`
|
|
StripeCustomerID string `json:"stripe_customer_id"`
|
|
}
|
|
|
|
// UpdateSubscriptionFromStripe updates subscription from Stripe webhook data
|
|
type UpdateSubscriptionFromStripe struct {
|
|
StripeSubscriptionID string `json:"stripe_subscription_id"`
|
|
StripePriceID *string `json:"stripe_price_id,omitempty"`
|
|
Status *SubscriptionStatus `json:"status,omitempty"`
|
|
CurrentPeriodStart *time.Time `json:"current_period_start,omitempty"`
|
|
CurrentPeriodEnd *time.Time `json:"current_period_end,omitempty"`
|
|
CancelAtPeriodEnd *bool `json:"cancel_at_period_end,omitempty"`
|
|
CanceledAt *time.Time `json:"canceled_at,omitempty"`
|
|
TrialStart *time.Time `json:"trial_start,omitempty"`
|
|
TrialEnd *time.Time `json:"trial_end,omitempty"`
|
|
}
|
|
|
|
// StripeWebhookEvent for idempotency tracking
|
|
type StripeWebhookEvent struct {
|
|
ID string `json:"id"`
|
|
EventType string `json:"event_type"`
|
|
ProcessedAt time.Time `json:"processed_at"`
|
|
Payload map[string]interface{} `json:"payload,omitempty"`
|
|
}
|