From e3f5ed4985a53262d57e7f4a4710104ed870b54c Mon Sep 17 00:00:00 2001 From: Michael Chihlas Date: Wed, 6 May 2026 03:59:19 -0400 Subject: [PATCH] feat(billing): add complimentary status, fix is_paid, add has_pro_entitlement Co-Authored-By: Claude Opus 4.7 --- backend/app/models/subscription.py | 16 +++++++- backend/tests/test_subscription_properties.py | 41 +++++++++++++++++++ 2 files changed, 55 insertions(+), 2 deletions(-) create mode 100644 backend/tests/test_subscription_properties.py 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