diff --git a/backend/app/models/subscription.py b/backend/app/models/subscription.py index 54b2a440..11024582 100644 --- a/backend/app/models/subscription.py +++ b/backend/app/models/subscription.py @@ -32,8 +32,20 @@ class Subscription(Base): @property def is_active(self) -> bool: - return self.status in ("active", "trialing") + return self.status in ("active", "trialing", "complimentary") @property def is_paid(self) -> bool: - return self.plan in ("pro", "team") + # Excludes complimentary and trialing so MRR/paid-customer metrics aren't inflated. + return self.plan in ("pro", "team") and self.status not in ("complimentary", "trialing") + + @property + def has_pro_entitlement(self) -> bool: + """True if the account can access Pro features right now.""" + if self.plan in ("pro", "team"): + if self.status in ("active", "complimentary"): + return True + if self.status == "trialing" and self.current_period_end is not None: + from datetime import datetime, timezone + return self.current_period_end > datetime.now(timezone.utc) + return False diff --git a/backend/tests/test_subscription_properties.py b/backend/tests/test_subscription_properties.py new file mode 100644 index 00000000..77d5716c --- /dev/null +++ b/backend/tests/test_subscription_properties.py @@ -0,0 +1,41 @@ +from datetime import datetime, timezone, timedelta +from app.models.subscription import Subscription + + +def make_sub(**kwargs): + sub = Subscription() + sub.plan = kwargs.get("plan", "free") + sub.status = kwargs.get("status", "active") + sub.current_period_end = kwargs.get("current_period_end") + return sub + + +def test_complimentary_is_active_but_not_paid(): + sub = make_sub(plan="pro", status="complimentary") + assert sub.is_active is True + assert sub.is_paid is False + assert sub.has_pro_entitlement is True + + +def test_paid_pro_active(): + sub = make_sub(plan="pro", status="active") + assert sub.is_paid is True + assert sub.has_pro_entitlement is True + + +def test_trial_unexpired_has_entitlement(): + sub = make_sub(plan="pro", status="trialing", current_period_end=datetime.now(timezone.utc) + timedelta(days=5)) + assert sub.is_active is True + assert sub.is_paid is False + assert sub.has_pro_entitlement is True + + +def test_trial_expired_no_entitlement(): + sub = make_sub(plan="pro", status="trialing", current_period_end=datetime.now(timezone.utc) - timedelta(hours=1)) + assert sub.has_pro_entitlement is False + + +def test_canceled_no_entitlement(): + sub = make_sub(plan="pro", status="canceled") + assert sub.is_active is False + assert sub.has_pro_entitlement is False