office_translator/services/payment_service.py
Sepehr fcabe882cd feat: Add complete monetization system
Backend:
- User authentication with JWT tokens (auth_service.py)
- Subscription plans: Free, Starter (), Pro (), Business (), Enterprise
- Stripe integration for payments (payment_service.py)
- Usage tracking and quotas
- Credit packages for pay-per-use
- Plan-based provider restrictions

Frontend:
- Landing page with hero, features, pricing preview (landing-sections.tsx)
- Pricing page with all plans and credit packages (/pricing)
- User dashboard with usage stats (/dashboard)
- Login/Register pages with validation (/auth/login, /auth/register)
- Ollama self-hosting setup guide (/ollama-setup)
- Updated sidebar with user section and plan badge

Monetization strategy:
- Freemium: 3 docs/day, Ollama only
- Starter: 50 docs/month, Google Translate
- Pro: 200 docs/month, all providers, API access
- Business: 1000 docs/month, team management
- Enterprise: Custom pricing, SLA

Self-hosted option:
- Free unlimited usage with own Ollama server
- Complete privacy (data never leaves machine)
- Step-by-step setup guide included
2025-11-30 21:11:51 +01:00

299 lines
9.4 KiB
Python

"""
Stripe payment integration for subscriptions and credits
"""
import os
from typing import Optional, Dict, Any
from datetime import datetime
# Try to import stripe
try:
import stripe
STRIPE_AVAILABLE = True
except ImportError:
STRIPE_AVAILABLE = False
stripe = None
from models.subscription import PlanType, PLANS, CREDIT_PACKAGES, SubscriptionStatus
from services.auth_service import get_user_by_id, update_user, add_credits
# Stripe configuration
STRIPE_SECRET_KEY = os.getenv("STRIPE_SECRET_KEY", "")
STRIPE_WEBHOOK_SECRET = os.getenv("STRIPE_WEBHOOK_SECRET", "")
STRIPE_PUBLISHABLE_KEY = os.getenv("STRIPE_PUBLISHABLE_KEY", "")
if STRIPE_AVAILABLE and STRIPE_SECRET_KEY:
stripe.api_key = STRIPE_SECRET_KEY
def is_stripe_configured() -> bool:
"""Check if Stripe is properly configured"""
return STRIPE_AVAILABLE and bool(STRIPE_SECRET_KEY)
async def create_checkout_session(
user_id: str,
plan: PlanType,
billing_period: str = "monthly", # monthly or yearly
success_url: str = "",
cancel_url: str = "",
) -> Optional[Dict[str, Any]]:
"""Create a Stripe checkout session for subscription"""
if not is_stripe_configured():
return {"error": "Stripe not configured", "demo_mode": True}
user = get_user_by_id(user_id)
if not user:
return {"error": "User not found"}
plan_config = PLANS[plan]
price_id = plan_config[f"stripe_price_id_{billing_period}"]
if not price_id:
return {"error": "Plan not available for purchase"}
try:
# Create or get Stripe customer
if user.stripe_customer_id:
customer_id = user.stripe_customer_id
else:
customer = stripe.Customer.create(
email=user.email,
name=user.name,
metadata={"user_id": user_id}
)
customer_id = customer.id
update_user(user_id, {"stripe_customer_id": customer_id})
# Create checkout session
session = stripe.checkout.Session.create(
customer=customer_id,
mode="subscription",
payment_method_types=["card"],
line_items=[{"price": price_id, "quantity": 1}],
success_url=success_url or f"{os.getenv('FRONTEND_URL', 'http://localhost:3000')}/dashboard?session_id={{CHECKOUT_SESSION_ID}}",
cancel_url=cancel_url or f"{os.getenv('FRONTEND_URL', 'http://localhost:3000')}/pricing",
metadata={"user_id": user_id, "plan": plan.value},
subscription_data={
"metadata": {"user_id": user_id, "plan": plan.value}
}
)
return {
"session_id": session.id,
"url": session.url
}
except Exception as e:
return {"error": str(e)}
async def create_credits_checkout(
user_id: str,
package_index: int,
success_url: str = "",
cancel_url: str = "",
) -> Optional[Dict[str, Any]]:
"""Create a Stripe checkout session for credit purchase"""
if not is_stripe_configured():
return {"error": "Stripe not configured", "demo_mode": True}
if package_index < 0 or package_index >= len(CREDIT_PACKAGES):
return {"error": "Invalid package"}
user = get_user_by_id(user_id)
if not user:
return {"error": "User not found"}
package = CREDIT_PACKAGES[package_index]
try:
# Create or get Stripe customer
if user.stripe_customer_id:
customer_id = user.stripe_customer_id
else:
customer = stripe.Customer.create(
email=user.email,
name=user.name,
metadata={"user_id": user_id}
)
customer_id = customer.id
update_user(user_id, {"stripe_customer_id": customer_id})
# Create checkout session
session = stripe.checkout.Session.create(
customer=customer_id,
mode="payment",
payment_method_types=["card"],
line_items=[{"price": package["stripe_price_id"], "quantity": 1}],
success_url=success_url or f"{os.getenv('FRONTEND_URL', 'http://localhost:3000')}/dashboard?credits=purchased",
cancel_url=cancel_url or f"{os.getenv('FRONTEND_URL', 'http://localhost:3000')}/pricing",
metadata={
"user_id": user_id,
"credits": package["credits"],
"type": "credits"
}
)
return {
"session_id": session.id,
"url": session.url
}
except Exception as e:
return {"error": str(e)}
async def handle_webhook(payload: bytes, sig_header: str) -> Dict[str, Any]:
"""Handle Stripe webhook events"""
if not is_stripe_configured():
return {"error": "Stripe not configured"}
try:
event = stripe.Webhook.construct_event(
payload, sig_header, STRIPE_WEBHOOK_SECRET
)
except ValueError:
return {"error": "Invalid payload"}
except stripe.error.SignatureVerificationError:
return {"error": "Invalid signature"}
# Handle the event
if event["type"] == "checkout.session.completed":
session = event["data"]["object"]
await handle_checkout_completed(session)
elif event["type"] == "customer.subscription.updated":
subscription = event["data"]["object"]
await handle_subscription_updated(subscription)
elif event["type"] == "customer.subscription.deleted":
subscription = event["data"]["object"]
await handle_subscription_deleted(subscription)
elif event["type"] == "invoice.payment_failed":
invoice = event["data"]["object"]
await handle_payment_failed(invoice)
return {"status": "success"}
async def handle_checkout_completed(session: Dict):
"""Handle successful checkout"""
metadata = session.get("metadata", {})
user_id = metadata.get("user_id")
if not user_id:
return
# Check if it's a credit purchase
if metadata.get("type") == "credits":
credits = int(metadata.get("credits", 0))
add_credits(user_id, credits)
return
# It's a subscription
plan = metadata.get("plan")
if plan:
subscription_id = session.get("subscription")
update_user(user_id, {
"plan": plan,
"subscription_status": SubscriptionStatus.ACTIVE.value,
"stripe_subscription_id": subscription_id,
"docs_translated_this_month": 0, # Reset on new subscription
"pages_translated_this_month": 0,
})
async def handle_subscription_updated(subscription: Dict):
"""Handle subscription updates"""
metadata = subscription.get("metadata", {})
user_id = metadata.get("user_id")
if not user_id:
return
status_map = {
"active": SubscriptionStatus.ACTIVE,
"past_due": SubscriptionStatus.PAST_DUE,
"canceled": SubscriptionStatus.CANCELED,
"trialing": SubscriptionStatus.TRIALING,
"paused": SubscriptionStatus.PAUSED,
}
stripe_status = subscription.get("status", "active")
status = status_map.get(stripe_status, SubscriptionStatus.ACTIVE)
update_user(user_id, {
"subscription_status": status.value,
"subscription_ends_at": datetime.fromtimestamp(
subscription.get("current_period_end", 0)
).isoformat() if subscription.get("current_period_end") else None
})
async def handle_subscription_deleted(subscription: Dict):
"""Handle subscription cancellation"""
metadata = subscription.get("metadata", {})
user_id = metadata.get("user_id")
if not user_id:
return
update_user(user_id, {
"plan": PlanType.FREE.value,
"subscription_status": SubscriptionStatus.CANCELED.value,
"stripe_subscription_id": None,
})
async def handle_payment_failed(invoice: Dict):
"""Handle failed payment"""
customer_id = invoice.get("customer")
if not customer_id:
return
# Find user by customer ID and update status
# In production, query database by stripe_customer_id
async def cancel_subscription(user_id: str) -> Dict[str, Any]:
"""Cancel a user's subscription"""
if not is_stripe_configured():
return {"error": "Stripe not configured"}
user = get_user_by_id(user_id)
if not user or not user.stripe_subscription_id:
return {"error": "No active subscription"}
try:
# Cancel at period end
subscription = stripe.Subscription.modify(
user.stripe_subscription_id,
cancel_at_period_end=True
)
return {
"status": "canceling",
"cancel_at": datetime.fromtimestamp(subscription.cancel_at).isoformat() if subscription.cancel_at else None
}
except Exception as e:
return {"error": str(e)}
async def get_billing_portal_url(user_id: str) -> Optional[str]:
"""Get Stripe billing portal URL for customer"""
if not is_stripe_configured():
return None
user = get_user_by_id(user_id)
if not user or not user.stripe_customer_id:
return None
try:
session = stripe.billing_portal.Session.create(
customer=user.stripe_customer_id,
return_url=f"{os.getenv('FRONTEND_URL', 'http://localhost:3000')}/dashboard"
)
return session.url
except:
return None