import json import uuid import os import threading import asyncio from dotenv import load_dotenv # Load environment variables from .env if it exists load_dotenv() from datetime import datetime # Production Secret Sync from fastapi import FastAPI, Request, BackgroundTasks from fastapi.middleware.cors import CORSMiddleware from fastapi.staticfiles import StaticFiles from fastapi.responses import FileResponse, JSONResponse, RedirectResponse from scraper import run_full_scan, search_google_play, search_app_store, ai_extract_search_keywords, ai_validate_match, ai_generate_battle_analysis from huggingface_hub import hf_hub_download import stripe # Stripe Configuration stripe.api_key = os.getenv("STRIPE_SECRET_KEY") STRIPE_WEBHOOK_SECRET = os.getenv("STRIPE_WEBHOOK_SECRET", "") # Price IDs - In production, these should come from environment variables PRICE_SINGLE = os.getenv("STRIPE_PRICE_SINGLE", "price_single_placeholder") PRICE_MONTHLY = os.getenv("STRIPE_PRICE_MONTHLY", "price_pro_monthly_placeholder") PRICE_YEARLY = os.getenv("STRIPE_PRICE_YEARLY", "price_max_yearly_placeholder") PRICE_METERED = os.getenv("STRIPE_PRICE_METERED", "price_metered_placeholder") app = FastAPI() # Enable CORS for the Firebase-hosted dashboard app.add_middleware( CORSMiddleware, allow_origins=["https://skrapyx.space", "http://localhost:5173", "http://localhost:8006"], allow_credentials=True, allow_methods=["*"], allow_headers=["*"], ) # Automated Model Sync (Hugging Face / Zero Cost) def ensure_model_is_present(): """Downloads the 4B Qwen 3.5 model from HF Hub if it's not present.""" model_dir = "ml_models" model_name = "qwen-3.5-4b.gguf" model_path = os.path.join(model_dir, model_name) if not os.path.exists(model_path): print(f"📥 [HF] AI Model missing. Downloading Qwen 3.5 (4B) from Hub...") os.makedirs(model_dir, exist_ok=True) try: # Using bartowski's high-quality GGUF as the source for Qwen 3.5 hf_hub_download( repo_id="bartowski/Qwen_Qwen3.5-4B-GGUF", filename="Qwen_Qwen3.5-4B-Q4_K_M.gguf", local_dir=model_dir, local_dir_use_symlinks=False ) # Standardize filename for the scraper downloaded_path = os.path.join(model_dir, "Qwen_Qwen3.5-4B-Q4_K_M.gguf") if os.path.exists(downloaded_path): os.rename(downloaded_path, model_path) print(f"✅ [HF] Qwen 3.5 successfully synchronized.") except Exception as e: print(f"❌ [HF] Model download failed: {e}") # Trigger model check on startup ensure_model_is_present() # Security Headers Middleware: Crucial for Firebase Auth and Stripe Popups @app.middleware("http") async def add_security_headers(request: Request, call_next): response = await call_next(request) # Enable Cross-Origin-Opener-Policy for synchronization popups response.headers["Cross-Origin-Opener-Policy"] = "same-origin-allow-popups" # Loosen COEP if needed, but for now allow cross-origin-resource response.headers["Cross-Origin-Resource-Policy"] = "cross-origin" return response # Usage-Based Metering: Reports activity to Stripe async def report_meter_usage(user_id: str, event_id: str, value: int = 1): """Reports usage event to Stripe Billing Meter for 'api_requests'""" try: user_doc = firebase_manager.db.collection("users").document(user_id).get() if not user_doc.exists: return user_data = user_doc.to_dict() customer_id = user_data.get("stripe_customer_id") if not customer_id: print(f"[Meter] No customer ID for {user_id}. Profile will self-heal on next dashboard load.") return # Report the meter event to Stripe stripe.billing.MeterEvent.create( event_name="api_requests", payload={ "value": str(value), "stripe_customer_id": customer_id, }, identifier=f"{event_id}_{int(datetime.now().timestamp())}" ) print(f"[Meter] Reported {value} units for Customer {customer_id} (Event: {event_id})") except stripe.error.InvalidRequestError as e: if "No such customer" in str(e): print(f"[Meter] INVALID CUSTOMER DETECTED ({customer_id}). Healing required.") else: print(f"[Meter] Stripe Error: {e}") except Exception as e: print(f"[Meter] Failed to report usage: {e}") from firebase_manager import firebase_manager storage_lock = threading.Lock() # Serve original Vanilla JS dashboard assets app.mount("/dashboard", StaticFiles(directory="dashboard"), name="dashboard") @app.get("/") async def read_index(): # Serve the legacy Neural Intelligence dashboard return FileResponse("dashboard/index.html") @app.get("/health") async def health_check(): return {"status": "ok", "timestamp": datetime.now().isoformat()} @app.get("/api/search/google") async def api_search_google(q: str, req: Request): user = await get_current_user(req) if not user: return JSONResponse({"error": "Unauthorized"}, status_code=401) return search_google_play(q) @app.get("/api/search/apple") async def api_search_apple(q: str, req: Request): user = await get_current_user(req) if not user: return JSONResponse({"error": "Unauthorized"}, status_code=401) return search_app_store(q) @app.get("/api/search/idea") async def api_search_idea(q: str, platform: str, req: Request): user = await get_current_user(req) if not user: return JSONResponse({"error": "Unauthorized"}, status_code=401) # Use AI to get 4-5 high intent keywords keywords_raw = ai_extract_search_keywords(q) keywords = [k.strip() for k in keywords_raw.split(",") if k.strip()] results = [] seen_ids = set() # Search for each keyword and aggregate for kw in keywords[:3]: # Limit to top 3 keywords for speed if platform == 'google': batch = search_google_play(kw) else: batch = search_app_store(kw) for app_item in batch: if app_item['id'] not in seen_ids: seen_ids.add(app_item['id']) results.append(app_item) return results @app.get("/api/agent/discover") async def api_agent_discover(q: str, req: Request): user = await get_current_user(req) if not user: return JSONResponse({"error": "Unauthorized"}, status_code=401) # 1. Neural Keyword Extraction keywords_raw = ai_extract_search_keywords(q) keywords = [k.strip() for k in keywords_raw.split(",") if k.strip()] if not keywords: keywords = [q] # 2. Parallel Store Search all_candidates = [] seen_ids = set() # Search for all keywords to get a broad pool for kw in keywords[:5]: gp_results = search_google_play(kw) as_results = search_app_store(kw) for r in gp_results + as_results: if r['id'] not in seen_ids: seen_ids.add(r['id']) all_candidates.append(r) # Sort candidates? For now return as is, or sorted by 'score' if available all_candidates.sort(key=lambda x: x.get('score', 0) or 0, reverse=True) return { "query": q, "keywords": keywords, "candidates": all_candidates[:15] # Return top 15 most relevant } # Auth Middleware Skeleton async def get_current_user(request: Request): auth_header = request.headers.get("Authorization") if not auth_header or not auth_header.startswith("Bearer "): return None id_token = auth_header.split(" ").pop() try: from firebase_admin import auth decoded_token = auth.verify_id_token(id_token) uid = decoded_token["uid"] email = decoded_token.get("email") name = decoded_token.get("name", "User") # Ensure user exists in Firestore and get their status user_profile = firebase_manager.get_or_create_user(uid, email, name) return user_profile except Exception as e: print(f"[Auth] Token Verification Failed: {e}") return None @app.get("/api/projects") async def get_projects_api(req: Request): user = await get_current_user(req) if not user: return JSONResponse({"error": "Unauthorized"}, status_code=401) return firebase_manager.get_projects(user_id=user["user_id"]) @app.get("/api/user/profile") async def get_user_profile(req: Request): user = await get_current_user(req) if not user: return JSONResponse({"error": "Unauthorized"}, status_code=401) profile = firebase_manager.get_or_create_user(user["user_id"], user["email"], user.get("name", "Operator")) # Automation: Proactive Stripe Customer sync with Revalidation customer_id = profile.get("stripe_customer_id") needs_recreation = False if customer_id: try: # Verify the customer exists in the CURRENT Stripe account (Live or Sandbox) stripe.Customer.retrieve(customer_id) except stripe.error.InvalidRequestError as e: if "No such customer" in str(e): print(f"[Automation] Customer {customer_id} not found in this account. Triggering recreation.") needs_recreation = True else: print(f"[Automation] Stripe verification error: {e}") except Exception: needs_recreation = True if not customer_id or needs_recreation: try: # Check if customer exists by email before creating new one customers = stripe.Customer.list(email=user["email"]).data if customers: customer_id = customers[0].id else: customer = stripe.Customer.create( email=user["email"], name=profile.get("name", "Operator"), metadata={"user_id": user["user_id"]} ) customer_id = customer.id # Update local and cloud profile firebase_manager.db.collection("users").document(user["user_id"]).update({"stripe_customer_id": customer_id}) profile["stripe_customer_id"] = customer_id except Exception as e: print(f"[Automation] Customer recreation failed: {e}") # Add plan metadata for frontend recognition profile["is_metered"] = profile.get("plan_type") == "metered" profile["is_pro"] = profile.get("is_pro") or profile.get("plan_type") in ["monthly", "yearly", "metered"] return profile @app.post("/api/user/profile") async def update_user_profile(req: Request): user = await get_current_user(req) if not user: return JSONResponse({"error": "Unauthorized"}, status_code=401) updates = await req.json() allowed_fields = ["name", "username"] filtered_updates = {k: v for k, v in updates.items() if k in allowed_fields} if not filtered_updates: return JSONResponse({"error": "No valid fields to update"}, status_code=400) with storage_lock: firebase_manager.db.collection("users").document(user["user_id"]).update(filtered_updates) # Fetch fresh profile to return profile = firebase_manager.get_or_create_user(user["user_id"], user["email"], user.get("name", "Operator")) return profile @app.get("/api/scan/{scan_id}") async def get_scan_data(scan_id: str, req: Request): user = await get_current_user(req) if not user: return JSONResponse({"error": "Unauthorized"}, status_code=401) data = firebase_manager.download_scan(scan_id) if data: return JSONResponse(data) return JSONResponse({"error": "Scan not found in cloud storage"}, status_code=404) @app.post("/api/projects") async def create_project(req: Request): user = await get_current_user(req) if not user: return JSONResponse({"error": "Unauthorized"}, status_code=401) data = await req.json() name = data.get("name") if not name: return {"error": "Name required"} with storage_lock: # Check for existing project with same IDs for this user existing_projects = firebase_manager.get_projects(user_id=user["user_id"]) gp_id = data.get("gp_id") as_id = data.get("as_id") for ep in existing_projects.values(): if (gp_id and ep.get("gp_id") == gp_id) or (as_id and ep.get("as_id") == as_id): return ep # Return existing project instead of creating duplicate pid = str(uuid.uuid4())[:8] project = { "id": pid, "user_id": user["user_id"], "name": name, "gp_id": gp_id, "as_id": as_id, "scans": [], "created_at": datetime.now().isoformat(), "is_pinned": False } firebase_manager.save_project(user["user_id"], pid, project) return project @app.patch("/api/projects/{proj_id}") async def update_project(proj_id: str, req: Request): user = await get_current_user(req) if not user: return JSONResponse({"error": "Unauthorized"}, status_code=401) updates = await req.json() with storage_lock: projects = firebase_manager.get_projects(user["user_id"]) if proj_id in projects: projects[proj_id].update(updates) firebase_manager.save_project(user["user_id"], proj_id, projects[proj_id]) return projects[proj_id] return {"error": "Not found"} @app.delete("/api/projects/{proj_id}") async def delete_project(proj_id: str, req: Request): user = await get_current_user(req) if not user: return JSONResponse({"error": "Unauthorized"}, status_code=401) with storage_lock: firebase_manager.delete_project(user["user_id"], proj_id) return {"status": "success"} @app.post("/api/projects/{proj_id}/link") async def link_project(proj_id: str, req: Request): user = await get_current_user(req) if not user: return JSONResponse({"error": "Unauthorized"}, status_code=401) data = await req.json() with storage_lock: projects = firebase_manager.get_projects(user_id=user["user_id"]) if proj_id in projects: if "gp_id" in data: projects[proj_id]["gp_id"] = data["gp_id"] if "as_id" in data: projects[proj_id]["as_id"] = data["as_id"] firebase_manager.save_project(user["user_id"], proj_id, projects[proj_id]) return projects[proj_id] return JSONResponse({"error": "Not found or unauthorized"}, status_code=403) @app.get("/api/jobs/{job_id}") async def get_job_status(job_id: str, req: Request): user = await get_current_user(req) if not user: return JSONResponse({"error": "Unauthorized"}, status_code=401) with storage_lock: jobs = firebase_manager.get_jobs(user["user_id"]) return jobs.get(job_id, {"status": "Not Found"}) async def background_scan_task(user_id: str, job_id: str, proj_id: str, platform: str, url: str, report_usage: bool = True): """Background worker for multi-platform extraction""" try: # Load project from personalized collection projects = firebase_manager.get_projects(user_id) proj = projects.get(proj_id) if not proj: firebase_manager.update_job(user_id, job_id, {"status": "Failed", "message": "Project not found"}) return # Phase 1: Scraping (30%) firebase_manager.update_job(user_id, job_id, {"status": "Processing", "progress": 15, "message": "Evaluating app ecosystems and locating neural scrapers..."}) gp_url = url if platform == 'Android' else None as_url = url if platform == 'iOS' else None firebase_manager.update_job(user_id, job_id, {"status": "Processing", "progress": 30, "message": f"Acquiring {platform} reviews for deep analysis..."}) payload = run_full_scan(gp_url, as_url) if not payload: firebase_manager.update_job(user_id, job_id, {"status": "Failed", "message": "No data found for this application."}) return if payload.get('mismatch_error'): gp_title = payload.get('gp_title') as_title = payload.get('as_title') firebase_manager.update_job(user_id, job_id, {"status": "Failed", "message": f"Security mismatch: {gp_title} vs {as_title}"}) return # Phase 2: Neural Analysis (70%) firebase_manager.update_job(user_id, job_id, {"status": "Processing", "progress": 60, "message": "Identifying clusters and identifying UX pain points..."}) # Phase 3: Synthesis (90%) firebase_manager.update_job(user_id, job_id, {"status": "Processing", "progress": 85, "message": "Synthesizing future growth recommendations..."}) # Save results to Firebase Storage scan_id = payload['id'] success = firebase_manager.upload_scan(scan_id, payload) if not success: firebase_manager.update_job(user_id, job_id, {"status": "Failed", "message": "Failed to persist results to Cloud Storage."}) return # Update project scans proj["scans"].append({ "id": scan_id, "platform": platform, "timestamp": payload["timestamp"], "count": len(payload["reviews"]) }) firebase_manager.save_project(user_id, proj_id, proj) firebase_manager.update_job(user_id, job_id, { "status": "Completed", "progress": 100, "scan_id": scan_id, "message": "Intelligence synthesis complete!" }) # Report actual successful usage to Stripe ONLY if it wasn't pre-paid if report_usage: await report_meter_usage(user_id, scan_id) except Exception as e: print(f"Background Task Error: {e}") firebase_manager.update_job(user_id, job_id, {"status": "Failed", "message": str(e)}) @app.post("/api/scan") async def create_scan(request: Request, background_tasks: BackgroundTasks): user = await get_current_user(request) if not user: return JSONResponse({"error": "Unauthorized"}, status_code=401) body = await request.json() proj_id = body.get("proj_id") platform = body.get("platform") # Credit & Metered Consent Enforcement if user.get("credits", 0) <= 0: if user.get("subscription_plan") == "metered": if not body.get("confirm_meter_charge"): return JSONResponse({ "error": "Meter Consent Required", "message": "Not enough credit. Do you want me to charge $0.50 for this store?" }, status_code=402) else: return JSONResponse({ "error": "Insufficient Credits", "message": "You have exhausted your one-time analysis trial. Please upgrade to a subscription to continue." }, status_code=402) with storage_lock: projects = firebase_manager.get_projects(user_id=user["user_id"]) if proj_id not in projects: return JSONResponse({"error": "Project not found or unauthorized"}, status_code=403) proj = projects[proj_id] gp_url = proj.get("gp_id") if platform == 'Android' else None as_url = proj.get("as_id") if platform == 'iOS' else None # Validation if platform == 'Android' and (not gp_url or str(gp_url).lower() == 'null'): return JSONResponse({"error": "Google Play ID is missing."}, status_code=400) if platform == 'iOS' and (not as_url or str(as_url).lower() == 'null'): return JSONResponse({"error": "App Store ID is missing."}, status_code=400) # Deduct Credit Transactionally success = firebase_manager.deduct_credit(user["user_id"]) if not success: return JSONResponse({"error": "Transaction failed or insufficient credits"}, status_code=402) job_id = str(uuid.uuid4())[:8] firebase_manager.update_job(user["user_id"], job_id, { "id": job_id, "status": "Scraping", "progress": 0, "message": f"Starting sweep for {platform}...", "created_at": datetime.now().isoformat() }) # Determine if we should report usage to Stripe Post-pay Meter # We do NOT report if user is Pro (flat rate) or Metered (who just paid €0.50 top-up) report_usage = not (user.get("is_pro") or user.get("subscription_plan") == "metered") background_tasks.add_task(background_scan_task, user["user_id"], job_id, proj_id, platform, gp_url if platform == 'Android' else as_url, report_usage=report_usage ) return {"job_id": job_id} @app.post("/api/create-checkout-session") async def create_checkout_session(request: Request): user = await get_current_user(request) if not user: return JSONResponse({"error": "Unauthorized"}, status_code=401) try: data = await request.json() plan_type = data.get("plan", "single") # single, monthly, yearly # Select Price ID based on plan type if plan_type == 'yearly': price_id = PRICE_YEARLY elif plan_type == 'monthly': price_id = PRICE_MONTHLY elif plan_type == 'metered': price_id = PRICE_METERED else: price_id = PRICE_SINGLE # single # Select Line Items if plan_type == 'metered': # Use price_data for an immediate one-time charge of €0.50 # instead of a metered subscription which shows $0 upfront. # This satisfies the user's need to see 0.50 at checkout. line_item = { "price_data": { "currency": "eur", "product": "prod_UMjI6rvv3Ogd5r", # Pay As You Go Product ID "unit_amount": 50, # 0.50 EUR in cents }, "quantity": 1 } mode = "payment" else: line_item = {"price": price_id, "quantity": 1} # Detect mode based on Stripe Price object p_obj = stripe.Price.retrieve(price_id) mode = "subscription" if p_obj.type == "recurring" else "payment" # Automated Checkout with Tax Compliance session_kwargs = { "payment_method_types": ['card'], "line_items": [line_item], "mode": mode, "success_url": f"{str(request.base_url).rstrip('/')}/api/checkout-success?session_id={{CHECKOUT_SESSION_ID}}", "cancel_url": f"{str(request.base_url).rstrip('/')}/?payment=cancelled", "automatic_tax": {'enabled': True}, "tax_id_collection": {'enabled': True}, "billing_address_collection": "auto", "client_reference_id": user["user_id"], "metadata": { 'user_id': user["user_id"], 'plan': 'basic' if plan_type == 'single' else plan_type } } if mode == "payment": session_kwargs["payment_intent_data"] = {"receipt_email": user["email"]} customer_id = user.get("stripe_customer_id") if customer_id: session_kwargs["customer"] = customer_id session_kwargs["customer_update"] = {"address": "auto", "name": "auto"} else: session_kwargs["customer_email"] = user["email"] session = stripe.checkout.Session.create(**session_kwargs) return JSONResponse({"id": session.id, "url": session.url}) except stripe.error.InvalidRequestError as e: error_msg = str(e) status_code = 400 if "No such price" in error_msg: print(f"[Stripe] Missing Price Config: {error_msg}") return JSONResponse({ "error": "Configuration Error", "message": f"Stripe says: {error_msg}. Please check your .env and Stripe Dashboard." }, status_code=status_code) except Exception as e: print(f"[Stripe] Checkout Error: {e}") return JSONResponse({"error": str(e)}, status_code=500) @app.post("/api/webhook") async def stripe_webhook(request: Request): payload = await request.body() sig_header = request.headers.get("stripe-signature") try: event = stripe.Webhook.construct_event( payload, sig_header, STRIPE_WEBHOOK_SECRET ) except ValueError as e: # Invalid payload return JSONResponse({"error": "Invalid payload"}, status_code=400) except stripe.error.SignatureVerificationError as e: # Invalid signature return JSONResponse({"error": "Invalid signature"}, status_code=400) # Handle the checkout.session.completed event if event['type'] == 'checkout.session.completed': session = event['data']['object'] user_id = session.get("client_reference_id") customer_id = session.get("customer") plan_type = session.get("metadata", {}).get("plan", "single") if user_id: # Idempotency check: Have we already fulfilled this session? if firebase_manager.get_purchase(user_id, session.id): print(f"[Webhook] Session {session.id} already fulfilled. Skipping.") return JSONResponse({"status": "already_fulfilled"}) # Store transaction record invoice_url = None if session.get("invoice"): invoice = stripe.Invoice.retrieve(session.get("invoice")) invoice_url = invoice.get("hosted_invoice_url") elif session.get("payment_intent"): # For single payments, use the receipt pi = stripe.PaymentIntent.retrieve(session.get("payment_intent")) invoice_url = pi.charges.data[0].receipt_url if pi.charges.data else None firebase_manager.record_transaction(user_id, session.id, { "user_id": user_id, "session_id": session.id, "plan": plan_type, "amount": session.amount_total / 100, "currency": session.currency.upper(), "invoice_url": invoice_url, "status": "paid", "created_at": datetime.now().isoformat() }) firebase_manager.save_purchase(user_id, session.id, { "user_id": user_id, "session_id": session.id, "plan": plan_type, "customer_id": customer_id, "fulfilled_at": datetime.now().isoformat() }) # Store customer ID in Firebase firebase_manager.db.collection("users").document(user_id).update({"stripe_customer_id": customer_id}) print(f"[Webhook] Fulfilling {plan_type} for user {user_id}") expiry_date = None subscription_id = session.get("subscription") if subscription_id: # For subscriptions, fetch the period end date sub = stripe.Subscription.retrieve(subscription_id) expiry_date = datetime.fromtimestamp(sub.current_period_end).isoformat() elif plan_type == 'single': # For single payments, maybe 24h access or just credits # But looking at logic, single = add credits pass if plan_type in ['monthly', 'yearly']: firebase_manager.upgrade_to_pro(user_id, expiry_date=expiry_date, plan_type=plan_type, subscription_id=subscription_id) elif plan_type == 'metered': # For metered mode (usage based), ensure they are enrolled # If they arrived via the 0.50 payment mode, they get 1 credit if not subscription_id: firebase_manager.add_credits(user_id, 1) firebase_manager.enroll_in_metered(user_id, subscription_id=subscription_id) else: firebase_manager.add_credits(user_id, 1) # Handle subscription updates (Renewals) elif event['type'] == 'customer.subscription.updated': subscription = event['data']['object'] customer_id = subscription.get("customer") expiry_date = datetime.fromtimestamp(subscription.current_period_end).isoformat() from google.cloud.firestore_v1.base_query import FieldFilter users = firebase_manager.db.collection("users").where(filter=FieldFilter("stripe_customer_id", "==", customer_id)).stream() for u in users: firebase_manager.upgrade_to_pro(u.id, expiry_date=expiry_date) print(f"[Webhook] Renewed subscription for user {u.id} until {expiry_date}") # Handle subscription deletion (Cancellation) elif event['type'] == 'customer.subscription.deleted': subscription = event['data']['object'] customer_id = subscription.get("customer") # Find user by customer_id and revoke Pro status from google.cloud.firestore_v1.base_query import FieldFilter users = firebase_manager.db.collection("users").where(filter=FieldFilter("stripe_customer_id", "==", customer_id)).stream() for u in users: firebase_manager.upgrade_to_pro(u.id, status=False, expiry_date=None, plan_type=None, subscription_id=None) print(f"[Webhook] Revoking Pro status for customer {customer_id}") return JSONResponse({"status": "success"}) @app.post("/api/create-portal-session") async def create_portal_session(req: Request): user = await get_current_user(req) if not user: return JSONResponse({"error": "Unauthorized"}, status_code=401) # Get user profile to find customer_id profile = firebase_manager.get_or_create_user(user["user_id"], user["email"], "Operator") customer_id = profile.get("stripe_customer_id") if not customer_id: return JSONResponse({"error": "No Stripe customer found for this account. Please upgrade first."}, status_code=404) try: portal_session = stripe.billing_portal.Session.create( customer=customer_id, return_url=f"{str(req.base_url).rstrip('/')}/?view=billing", ) return {"url": portal_session.url} except Exception as e: return JSONResponse({"error": str(e)}, status_code=500) @app.get("/api/user/usage") async def get_user_usage(req: Request): user = await get_current_user(req) if not user: return JSONResponse({"error": "Unauthorized"}, status_code=401) customer_id = user.get("stripe_customer_id") if not customer_id: # Fallback to email search customers = stripe.Customer.list(email=user["email"]).data if customers: customer_id = customers[0].id else: return {"usage": 0} try: # List summaries for the 'api_requests' meter # Note: In production, meter ID should be used or mapped # For this demo, we'll try to find the meter first meters = stripe.billing.Meter.list(limit=10).data target_meter = next((m for m in meters if m.event_name == "api_requests"), None) if not target_meter: return {"usage": 0} summaries = stripe.billing.Meter.list_event_summaries( target_meter.id, customer=customer_id, start_time=int(datetime.now().replace(day=1, hour=0, minute=0, second=0).timestamp()), end_time=int(datetime.now().timestamp()), limit=1 ).data total_usage = summaries[0].aggregated_value if summaries else 0 return {"usage": total_usage} except Exception as e: print(f"Usage Fetch Error: {e}") return {"usage": 0} @app.get("/api/user/transactions") async def get_user_transactions(req: Request): user = await get_current_user(req) if not user: return JSONResponse({"error": "Unauthorized"}, status_code=401) return firebase_manager.get_transactions(user["user_id"]) @app.get("/api/checkout-success") async def checkout_success(session_id: str, request: Request): try: session = stripe.checkout.Session.retrieve(session_id) user_id = session.client_reference_id plan_type = getattr(session.metadata, "plan", "single") if session.metadata else "single" if user_id: # Idempotency check: Have we already fulfilled this session? if firebase_manager.get_purchase(user_id, session.id): print(f"[Success Redirect] Session {session.id} already fulfilled. Just redirecting.") return RedirectResponse(url="/?payment=success") # Store purchase record firebase_manager.save_purchase(user_id, session.id, { "user_id": user_id, "session_id": session.id, "plan": plan_type, "customer_id": session.customer, "fulfilled_at": datetime.now().isoformat() }) if plan_type in ['monthly', 'yearly']: # Grant Pro Status firebase_manager.upgrade_to_pro(user_id) else: # Grant Single Credit firebase_manager.add_credits(user_id, 1) return RedirectResponse(url="/?payment=success") except Exception as e: print(f"Checkout Success Error: {e}") return RedirectResponse(url="/?payment=error") @app.post("/api/battle") async def start_battle(request: Request, background_tasks: BackgroundTasks): user = await get_current_user(request) if not user: return JSONResponse({"error": "Unauthorized"}, status_code=401) try: body = await request.json() scan_id_a = body.get("scan_id_a") scan_id_b = body.get("scan_id_b") if not scan_id_a or not scan_id_b: return JSONResponse({"error": "Two scan IDs required for battle."}, status_code=400) # 1. Credit & Metered Check (Same as /api/scan) if user.get("credits", 0) <= 0: if user.get("subscription_plan") == "metered": if not body.get("confirm_meter_charge"): return JSONResponse({ "error": "Meter Consent Required", "message": "Not enough credit for Market Battle. Charge €0.50?" }, status_code=402) else: return JSONResponse({ "error": "Insufficient Credits", "message": "You need 1 analysis credit to start a Market Battle." }, status_code=402) # 2. Deduct Credit success = firebase_manager.deduct_credit(user["user_id"]) if not success: return JSONResponse({"error": "Transaction failed or insufficient credits"}, status_code=402) data_a = firebase_manager.download_scan(scan_id_a) data_b = firebase_manager.download_scan(scan_id_b) if not data_a or not data_b: return JSONResponse({"error": "One or more scans not found."}, status_code=404) # Perform comparative analysis clash_report = ai_generate_battle_analysis(data_a, data_b) # 3. Conditional Usage Reporting report_usage = not (user.get("is_pro") or user.get("subscription_plan") == "metered") if report_usage: background_tasks.add_task(report_meter_usage, user["user_id"], f"battle_{scan_id_a}_{scan_id_b}") return { "clash_report": clash_report, "app_a": { "name": data_a.get('app_name'), "synthesis": data_a.get('neural_synthesis') }, "app_b": { "name": data_b.get('app_name'), "synthesis": data_b.get('neural_synthesis') } } except Exception as e: print(f"Battle Logic Error: {e}") return JSONResponse({"error": str(e)}, status_code=500) if __name__ == "__main__": import uvicorn # Use 0.0.0.0 to allow external access in containers/deployment host = os.getenv("HOST", "0.0.0.0") port = int(os.getenv("PORT", 8006)) uvicorn.run("app:app", host=host, port=port, reload=False)