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:
|
||||
- Scout (Free): $0/month
|
||||
- Trader: €19/month (or ~$21)
|
||||
- Tycoon: €49/month (or ~$54)
|
||||
- Trader: $9/month
|
||||
- Tycoon: $29/month
|
||||
|
||||
Environment Variables Required:
|
||||
- STRIPE_SECRET_KEY: Stripe API secret key
|
||||
@ -24,7 +24,7 @@ from sqlalchemy import select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.models.user import User
|
||||
from app.models.subscription import Subscription
|
||||
from app.models.subscription import Subscription, SubscriptionTier, SubscriptionStatus
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@ -258,7 +258,7 @@ class StripeService:
|
||||
async def _handle_checkout_complete(data: Dict, db: AsyncSession):
|
||||
"""Handle successful checkout - activate subscription."""
|
||||
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")
|
||||
subscription_id = data.get("subscription")
|
||||
|
||||
@ -266,6 +266,14 @@ class StripeService:
|
||||
logger.error("Missing user_id or plan in checkout metadata")
|
||||
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
|
||||
result = await db.execute(
|
||||
select(User).where(User.id == int(user_id))
|
||||
@ -285,11 +293,11 @@ class StripeService:
|
||||
)
|
||||
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:
|
||||
subscription.tier = plan
|
||||
subscription.is_active = True
|
||||
subscription.tier = tier_enum # Use enum, not string
|
||||
subscription.status = SubscriptionStatus.ACTIVE
|
||||
subscription.stripe_subscription_id = subscription_id
|
||||
subscription.max_domains = tier_info["max_domains"]
|
||||
subscription.check_frequency = tier_info["check_frequency"]
|
||||
@ -297,8 +305,8 @@ class StripeService:
|
||||
else:
|
||||
subscription = Subscription(
|
||||
user_id=user.id,
|
||||
tier=plan,
|
||||
is_active=True,
|
||||
tier=tier_enum, # Use enum, not string
|
||||
status=SubscriptionStatus.ACTIVE,
|
||||
stripe_subscription_id=subscription_id,
|
||||
max_domains=tier_info["max_domains"],
|
||||
check_frequency=tier_info["check_frequency"],
|
||||
@ -340,8 +348,8 @@ class StripeService:
|
||||
subscription = result.scalar_one_or_none()
|
||||
|
||||
if subscription:
|
||||
subscription.tier = "scout"
|
||||
subscription.is_active = True # Scout is still active
|
||||
subscription.tier = SubscriptionTier.SCOUT # Use enum
|
||||
subscription.status = SubscriptionStatus.ACTIVE # Scout is still active
|
||||
subscription.stripe_subscription_id = None
|
||||
subscription.max_domains = TIER_FEATURES["scout"]["max_domains"]
|
||||
subscription.check_frequency = TIER_FEATURES["scout"]["check_frequency"]
|
||||
|
||||
@ -25,7 +25,7 @@ const tiers = [
|
||||
{ text: 'Market Feed', highlight: false, available: true, sublabel: '🌪️ Raw' },
|
||||
{ text: 'Alert Speed', highlight: false, available: true, sublabel: '🐢 Daily' },
|
||||
{ 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',
|
||||
highlighted: false,
|
||||
@ -45,8 +45,8 @@ const tiers = [
|
||||
{ text: 'Alert Speed', highlight: true, available: true, sublabel: '🐇 Hourly' },
|
||||
{ text: 'Renewal Price Intel', highlight: true, available: true },
|
||||
{ text: 'Pounce Score', highlight: true, available: true },
|
||||
{ text: 'List domains', highlight: true, available: true, sublabel: '0% Fee' },
|
||||
{ text: '5 Sniper Alerts', highlight: true, available: true },
|
||||
{ text: '5 Listings', highlight: true, available: true, sublabel: '0% Fee' },
|
||||
{ text: '10 Sniper Alerts', highlight: true, available: true },
|
||||
{ text: 'Portfolio (25 domains)', highlight: true, available: true },
|
||||
],
|
||||
cta: 'Upgrade to Trader',
|
||||
@ -65,10 +65,10 @@ const tiers = [
|
||||
{ text: '500 Watchlist Domains', highlight: true, available: true },
|
||||
{ text: 'Market Feed', highlight: true, available: true, sublabel: '⚡ Priority' },
|
||||
{ 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: 'Featured Listings', highlight: true, available: true, sublabel: '50 slots' },
|
||||
{ text: 'Unlimited Sniper Alerts', highlight: true, available: true },
|
||||
{ text: '50 Listings', highlight: true, available: true, sublabel: 'Featured' },
|
||||
{ text: '50 Sniper Alerts', highlight: true, available: true },
|
||||
{ text: 'Full Price History', highlight: true, available: true },
|
||||
],
|
||||
cta: 'Go Tycoon',
|
||||
@ -82,10 +82,10 @@ const comparisonFeatures = [
|
||||
{ name: 'Market Feed', scout: 'Raw', trader: 'Curated', tycoon: 'Priority' },
|
||||
{ name: 'Alert Speed', scout: 'Daily', trader: 'Hourly', tycoon: '10 min' },
|
||||
{ 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: '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' },
|
||||
]
|
||||
|
||||
|
||||
@ -54,8 +54,8 @@ export default function MyListingsPage() {
|
||||
const [menuOpen, setMenuOpen] = useState(false)
|
||||
|
||||
const tier = subscription?.tier || 'scout'
|
||||
const listingLimits: Record<string, number> = { scout: 3, trader: 25, tycoon: 100 }
|
||||
const maxListings = listingLimits[tier] || 3
|
||||
const listingLimits: Record<string, number> = { scout: 0, trader: 5, tycoon: 50 }
|
||||
const maxListings = listingLimits[tier] || 0
|
||||
const canAddMore = listings.length < maxListings
|
||||
const isTycoon = tier === 'tycoon'
|
||||
|
||||
|
||||
Reference in New Issue
Block a user