fix: Stripe USD prices + tier limits alignment
Some checks failed
CI / Frontend Lint & Type Check (push) Has been cancelled
CI / Frontend Build (push) Has been cancelled
CI / Backend Lint (push) Has been cancelled
CI / Backend Tests (push) Has been cancelled
CI / Docker Build (push) Has been cancelled
CI / Security Scan (push) Has been cancelled
Deploy / Build & Push Images (push) Has been cancelled
Deploy / Deploy to Server (push) Has been cancelled
Deploy / Notify (push) Has been cancelled
Some checks failed
CI / Frontend Lint & Type Check (push) Has been cancelled
CI / Frontend Build (push) Has been cancelled
CI / Backend Lint (push) Has been cancelled
CI / Backend Tests (push) Has been cancelled
CI / Docker Build (push) Has been cancelled
CI / Security Scan (push) Has been cancelled
Deploy / Build & Push Images (push) Has been cancelled
Deploy / Deploy to Server (push) Has been cancelled
Deploy / Notify (push) Has been cancelled
This commit is contained in:
@ -5,8 +5,8 @@ Handles subscription payments for pounce.ch
|
|||||||
|
|
||||||
Subscription Tiers:
|
Subscription Tiers:
|
||||||
- Scout (Free): $0/month
|
- Scout (Free): $0/month
|
||||||
- Trader: €19/month (or ~$21)
|
- Trader: $9/month
|
||||||
- Tycoon: €49/month (or ~$54)
|
- Tycoon: $29/month
|
||||||
|
|
||||||
Environment Variables Required:
|
Environment Variables Required:
|
||||||
- STRIPE_SECRET_KEY: Stripe API secret key
|
- STRIPE_SECRET_KEY: Stripe API secret key
|
||||||
@ -24,7 +24,7 @@ from sqlalchemy import select
|
|||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
from app.models.user import User
|
from app.models.user import User
|
||||||
from app.models.subscription import Subscription
|
from app.models.subscription import Subscription, SubscriptionTier, SubscriptionStatus
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@ -258,7 +258,7 @@ class StripeService:
|
|||||||
async def _handle_checkout_complete(data: Dict, db: AsyncSession):
|
async def _handle_checkout_complete(data: Dict, db: AsyncSession):
|
||||||
"""Handle successful checkout - activate subscription."""
|
"""Handle successful checkout - activate subscription."""
|
||||||
user_id = data.get("metadata", {}).get("user_id")
|
user_id = data.get("metadata", {}).get("user_id")
|
||||||
plan = data.get("metadata", {}).get("plan")
|
plan = data.get("metadata", {}).get("plan") # "trader" or "tycoon"
|
||||||
customer_id = data.get("customer")
|
customer_id = data.get("customer")
|
||||||
subscription_id = data.get("subscription")
|
subscription_id = data.get("subscription")
|
||||||
|
|
||||||
@ -266,6 +266,14 @@ class StripeService:
|
|||||||
logger.error("Missing user_id or plan in checkout metadata")
|
logger.error("Missing user_id or plan in checkout metadata")
|
||||||
return
|
return
|
||||||
|
|
||||||
|
# Convert plan string to SubscriptionTier enum
|
||||||
|
tier_map = {
|
||||||
|
"trader": SubscriptionTier.TRADER,
|
||||||
|
"tycoon": SubscriptionTier.TYCOON,
|
||||||
|
"scout": SubscriptionTier.SCOUT,
|
||||||
|
}
|
||||||
|
tier_enum = tier_map.get(plan.lower(), SubscriptionTier.SCOUT)
|
||||||
|
|
||||||
# Get user
|
# Get user
|
||||||
result = await db.execute(
|
result = await db.execute(
|
||||||
select(User).where(User.id == int(user_id))
|
select(User).where(User.id == int(user_id))
|
||||||
@ -285,11 +293,11 @@ class StripeService:
|
|||||||
)
|
)
|
||||||
subscription = sub_result.scalar_one_or_none()
|
subscription = sub_result.scalar_one_or_none()
|
||||||
|
|
||||||
tier_info = TIER_FEATURES.get(plan, TIER_FEATURES["scout"])
|
tier_info = TIER_FEATURES.get(plan.lower(), TIER_FEATURES["scout"])
|
||||||
|
|
||||||
if subscription:
|
if subscription:
|
||||||
subscription.tier = plan
|
subscription.tier = tier_enum # Use enum, not string
|
||||||
subscription.is_active = True
|
subscription.status = SubscriptionStatus.ACTIVE
|
||||||
subscription.stripe_subscription_id = subscription_id
|
subscription.stripe_subscription_id = subscription_id
|
||||||
subscription.max_domains = tier_info["max_domains"]
|
subscription.max_domains = tier_info["max_domains"]
|
||||||
subscription.check_frequency = tier_info["check_frequency"]
|
subscription.check_frequency = tier_info["check_frequency"]
|
||||||
@ -297,8 +305,8 @@ class StripeService:
|
|||||||
else:
|
else:
|
||||||
subscription = Subscription(
|
subscription = Subscription(
|
||||||
user_id=user.id,
|
user_id=user.id,
|
||||||
tier=plan,
|
tier=tier_enum, # Use enum, not string
|
||||||
is_active=True,
|
status=SubscriptionStatus.ACTIVE,
|
||||||
stripe_subscription_id=subscription_id,
|
stripe_subscription_id=subscription_id,
|
||||||
max_domains=tier_info["max_domains"],
|
max_domains=tier_info["max_domains"],
|
||||||
check_frequency=tier_info["check_frequency"],
|
check_frequency=tier_info["check_frequency"],
|
||||||
@ -340,8 +348,8 @@ class StripeService:
|
|||||||
subscription = result.scalar_one_or_none()
|
subscription = result.scalar_one_or_none()
|
||||||
|
|
||||||
if subscription:
|
if subscription:
|
||||||
subscription.tier = "scout"
|
subscription.tier = SubscriptionTier.SCOUT # Use enum
|
||||||
subscription.is_active = True # Scout is still active
|
subscription.status = SubscriptionStatus.ACTIVE # Scout is still active
|
||||||
subscription.stripe_subscription_id = None
|
subscription.stripe_subscription_id = None
|
||||||
subscription.max_domains = TIER_FEATURES["scout"]["max_domains"]
|
subscription.max_domains = TIER_FEATURES["scout"]["max_domains"]
|
||||||
subscription.check_frequency = TIER_FEATURES["scout"]["check_frequency"]
|
subscription.check_frequency = TIER_FEATURES["scout"]["check_frequency"]
|
||||||
|
|||||||
@ -25,7 +25,7 @@ const tiers = [
|
|||||||
{ text: 'Market Feed', highlight: false, available: true, sublabel: '🌪️ Raw' },
|
{ text: 'Market Feed', highlight: false, available: true, sublabel: '🌪️ Raw' },
|
||||||
{ text: 'Alert Speed', highlight: false, available: true, sublabel: '🐢 Daily' },
|
{ text: 'Alert Speed', highlight: false, available: true, sublabel: '🐢 Daily' },
|
||||||
{ text: 'Pounce Score', highlight: false, available: false },
|
{ text: 'Pounce Score', highlight: false, available: false },
|
||||||
{ text: 'Sniper Alerts', highlight: false, available: false },
|
{ text: '2 Sniper Alerts', highlight: false, available: true },
|
||||||
],
|
],
|
||||||
cta: 'Start Free',
|
cta: 'Start Free',
|
||||||
highlighted: false,
|
highlighted: false,
|
||||||
@ -45,8 +45,8 @@ const tiers = [
|
|||||||
{ text: 'Alert Speed', highlight: true, available: true, sublabel: '🐇 Hourly' },
|
{ text: 'Alert Speed', highlight: true, available: true, sublabel: '🐇 Hourly' },
|
||||||
{ text: 'Renewal Price Intel', highlight: true, available: true },
|
{ text: 'Renewal Price Intel', highlight: true, available: true },
|
||||||
{ text: 'Pounce Score', highlight: true, available: true },
|
{ text: 'Pounce Score', highlight: true, available: true },
|
||||||
{ text: 'List domains', highlight: true, available: true, sublabel: '0% Fee' },
|
{ text: '5 Listings', highlight: true, available: true, sublabel: '0% Fee' },
|
||||||
{ text: '5 Sniper Alerts', highlight: true, available: true },
|
{ text: '10 Sniper Alerts', highlight: true, available: true },
|
||||||
{ text: 'Portfolio (25 domains)', highlight: true, available: true },
|
{ text: 'Portfolio (25 domains)', highlight: true, available: true },
|
||||||
],
|
],
|
||||||
cta: 'Upgrade to Trader',
|
cta: 'Upgrade to Trader',
|
||||||
@ -65,10 +65,10 @@ const tiers = [
|
|||||||
{ text: '500 Watchlist Domains', highlight: true, available: true },
|
{ text: '500 Watchlist Domains', highlight: true, available: true },
|
||||||
{ text: 'Market Feed', highlight: true, available: true, sublabel: '⚡ Priority' },
|
{ text: 'Market Feed', highlight: true, available: true, sublabel: '⚡ Priority' },
|
||||||
{ text: 'Alert Speed', highlight: true, available: true, sublabel: '⚡ 10 min' },
|
{ text: 'Alert Speed', highlight: true, available: true, sublabel: '⚡ 10 min' },
|
||||||
{ text: 'Full Portfolio Monitor', highlight: true, available: true },
|
{ text: 'Unlimited Portfolio', highlight: true, available: true },
|
||||||
{ text: 'Score + SEO Data', highlight: true, available: true },
|
{ text: 'Score + SEO Data', highlight: true, available: true },
|
||||||
{ text: 'Featured Listings', highlight: true, available: true, sublabel: '50 slots' },
|
{ text: '50 Listings', highlight: true, available: true, sublabel: 'Featured' },
|
||||||
{ text: 'Unlimited Sniper Alerts', highlight: true, available: true },
|
{ text: '50 Sniper Alerts', highlight: true, available: true },
|
||||||
{ text: 'Full Price History', highlight: true, available: true },
|
{ text: 'Full Price History', highlight: true, available: true },
|
||||||
],
|
],
|
||||||
cta: 'Go Tycoon',
|
cta: 'Go Tycoon',
|
||||||
@ -82,10 +82,10 @@ const comparisonFeatures = [
|
|||||||
{ name: 'Market Feed', scout: 'Raw', trader: 'Curated', tycoon: 'Priority' },
|
{ name: 'Market Feed', scout: 'Raw', trader: 'Curated', tycoon: 'Priority' },
|
||||||
{ name: 'Alert Speed', scout: 'Daily', trader: 'Hourly', tycoon: '10 min' },
|
{ name: 'Alert Speed', scout: 'Daily', trader: 'Hourly', tycoon: '10 min' },
|
||||||
{ name: 'Watchlist', scout: '5 Domains', trader: '50 Domains', tycoon: '500 Domains' },
|
{ name: 'Watchlist', scout: '5 Domains', trader: '50 Domains', tycoon: '500 Domains' },
|
||||||
{ name: 'Marketplace', scout: 'Buy Only', trader: 'Sell (0% Fee)', tycoon: 'Sell + Featured' },
|
{ name: 'Listings', scout: '—', trader: '5 (0% Fee)', tycoon: '50 Featured' },
|
||||||
{ name: 'TLD Intel', scout: 'Public Trends', trader: 'Renewal Prices', tycoon: 'Full History' },
|
{ name: 'TLD Intel', scout: 'Public Trends', trader: 'Renewal Prices', tycoon: 'Full History' },
|
||||||
{ name: 'Valuation', scout: 'Locked', trader: 'Pounce Score', tycoon: 'Score + SEO' },
|
{ name: 'Valuation', scout: 'Locked', trader: 'Pounce Score', tycoon: 'Score + SEO' },
|
||||||
{ name: 'Sniper Alerts', scout: '—', trader: '5', tycoon: 'Unlimited' },
|
{ name: 'Sniper Alerts', scout: '2', trader: '10', tycoon: '50' },
|
||||||
{ name: 'Portfolio', scout: '—', trader: '25 Domains', tycoon: 'Unlimited' },
|
{ name: 'Portfolio', scout: '—', trader: '25 Domains', tycoon: 'Unlimited' },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
@ -54,8 +54,8 @@ export default function MyListingsPage() {
|
|||||||
const [menuOpen, setMenuOpen] = useState(false)
|
const [menuOpen, setMenuOpen] = useState(false)
|
||||||
|
|
||||||
const tier = subscription?.tier || 'scout'
|
const tier = subscription?.tier || 'scout'
|
||||||
const listingLimits: Record<string, number> = { scout: 3, trader: 25, tycoon: 100 }
|
const listingLimits: Record<string, number> = { scout: 0, trader: 5, tycoon: 50 }
|
||||||
const maxListings = listingLimits[tier] || 3
|
const maxListings = listingLimits[tier] || 0
|
||||||
const canAddMore = listings.length < maxListings
|
const canAddMore = listings.length < maxListings
|
||||||
const isTycoon = tier === 'tycoon'
|
const isTycoon = tier === 'tycoon'
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user