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

This commit is contained in:
2025-12-13 16:29:06 +01:00
parent f4e4595cde
commit 83aaca0721
3 changed files with 29 additions and 21 deletions

View File

@ -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"]

View File

@ -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' },
] ]

View File

@ -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'