import os, uuid, asyncpg, stripe, httpx from fastapi import FastAPI, HTTPException, Request from fastapi.middleware.cors import CORSMiddleware from pydantic import BaseModel app = FastAPI() def _parse_cors_allowlist(env_value: str | None) -> list[str]: if not env_value: return [] return [origin.strip() for origin in env_value.split(",") if origin.strip()] CORS_ALLOWED_ORIGINS = _parse_cors_allowlist(os.getenv("CORS_ALLOWED_ORIGINS")) app.add_middleware( CORSMiddleware, allow_origins=CORS_ALLOWED_ORIGINS, allow_credentials=True, allow_methods=["*"], allow_headers=["*"], ) # --- ENV --- DATABASE_URL = os.getenv("DATABASE_URL") # Stripe stripe.api_key = os.getenv("STRIPE_SECRET_KEY") STRIPE_PRICE_ID_PRO = os.getenv("STRIPE_PRICE_ID_PRO") STRIPE_PRICE_ID_STUDIO = os.getenv("STRIPE_PRICE_ID_STUDIO") # PayPal PAYPAL_ENV = os.getenv("PAYPAL_ENV", "sandbox") PAYPAL_CLIENT_ID = os.getenv("PAYPAL_CLIENT_ID") PAYPAL_CLIENT_SECRET = os.getenv("PAYPAL_CLIENT_SECRET") PAYPAL_BASE = "https://api-m.paypal.com" if PAYPAL_ENV == "live" else "https://api-m.sandbox.paypal.com" # Square SQUARE_ENV = os.getenv("SQUARE_ENV", "sandbox") SQUARE_ACCESS_TOKEN = os.getenv("SQUARE_ACCESS_TOKEN") SQUARE_LOCATION_ID = os.getenv("SQUARE_LOCATION_ID") SQUARE_API = "https://connect.squareup.com" if SQUARE_ENV=="production" else "https://connect.squareupsandbox.com" SQUARE_VERSION = "2025-05-15" # --- Startup/shutdown DB pool --- @app.on_event("startup") async def startup(): if DATABASE_URL: app.state.pool = await asyncpg.create_pool(DATABASE_URL) @app.on_event("shutdown") async def shutdown(): if hasattr(app.state, "pool"): await app.state.pool.close() # --- Models --- class MusicRequest(BaseModel): prompt: str class PlanBody(BaseModel): plan: str amount: float | None = None class CaptureBody(BaseModel): order_id: str # --- Helpers --- async def paypal_access_token(): async with httpx.AsyncClient() as client: r = await client.post( f"{PAYPAL_BASE}/v1/oauth2/token", data={"grant_type":"client_credentials"}, auth=(PAYPAL_CLIENT_ID, PAYPAL_CLIENT_SECRET), timeout=15 ) if r.status_code != 200: raise HTTPException(500, f"PayPal token error: {r.text}") return r.json()["access_token"] PLAN_AMOUNTS = { "pro": "19.00", "studio": "49.00", } def _validate_plan(plan: str) -> str: if plan in PLAN_AMOUNTS: return plan raise HTTPException(400, f"Unsupported plan '{plan}'") def plan_amount_usd(plan: str, override: float | None) -> str: _validate_plan(plan) if override is not None: return f"{override:.2f}" return PLAN_AMOUNTS[plan] # --- API --- @app.get("/api/status") async def status(): return { "ok": True, "db": bool(DATABASE_URL), "stripe": bool(stripe.api_key), "paypal": bool(PAYPAL_CLIENT_ID), "square": bool(SQUARE_ACCESS_TOKEN), } @app.post("/api/music/generate") async def generate_music(req: MusicRequest): if not hasattr(app.state,"pool"): return {"error":"no DB"} async with app.state.pool.acquire() as conn: track_id = await conn.fetchval("INSERT INTO tracks(prompt,status) VALUES($1,$2) RETURNING id", req.prompt,"processing") return {"track_id": track_id, "status": "processing"} @app.get("/tracks") async def list_tracks(): if not hasattr(app.state,"pool"): return [] async with app.state.pool.acquire() as conn: rows = await conn.fetch("SELECT id,prompt,status,created_at FROM tracks ORDER BY id DESC LIMIT 100") return [dict(r) for r in rows] # --- Stripe --- @app.post("/checkout") async def checkout(body: PlanBody): plan = _validate_plan(body.plan) price_id_map = { "pro": STRIPE_PRICE_ID_PRO, "studio": STRIPE_PRICE_ID_STUDIO, } price_id = price_id_map.get(plan) if not price_id: raise HTTPException(500, f"Stripe price ID missing for plan '{plan}'") session = stripe.checkout.Session.create( mode="subscription", line_items=[{"price": price_id,"quantity":1}], success_url="http://localhost:3000/success", cancel_url="http://localhost:3000/cancel" ) return {"url": session.url} # --- PayPal (Venmo) --- @app.post("/paypal/create-order") async def paypal_create_order(body: PlanBody): amount = plan_amount_usd(body.plan, body.amount) token = await paypal_access_token() payload = {"intent":"CAPTURE","purchase_units":[{"amount":{"currency_code":"USD","value":amount}}]} async with httpx.AsyncClient() as client: r = await client.post(f"{PAYPAL_BASE}/v2/checkout/orders",json=payload,headers={"Authorization":f"Bearer {token}"},timeout=15) if r.status_code not in (200,201): raise HTTPException(500,r.text) return r.json() @app.post("/paypal/capture-order") async def paypal_capture_order(body: CaptureBody): token = await paypal_access_token() async with httpx.AsyncClient() as client: r = await client.post(f"{PAYPAL_BASE}/v2/checkout/orders/{body.order_id}/capture",headers={"Authorization":f"Bearer {token}"},timeout=15) if r.status_code not in (200,201): raise HTTPException(500,r.text) return r.json() # --- Square (Cash App Pay) --- @app.post("/square/create-payment-link") async def square_create_payment_link(body: PlanBody): import httpx amount = plan_amount_usd(body.plan, body.amount) payload = { "idempotency_key": str(uuid.uuid4()), "quick_pay": { "name": f"AI Music {body.plan.capitalize()}", "price_money": {"amount": int(float(amount)*100), "currency": "USD"}, "location_id": SQUARE_LOCATION_ID, } } headers = {"Authorization":f"Bearer {SQUARE_ACCESS_TOKEN}","Square-Version":SQUARE_VERSION} async with httpx.AsyncClient() as client: r = await client.post(f"{SQUARE_API}/v2/online-checkout/payment-links",json=payload,headers=headers,timeout=15) if r.status_code not in (200,201): raise HTTPException(500,r.text) return r.json()