diff --git a/backend/main.py b/backend/main.py index 88bae85..de28b83 100644 --- a/backend/main.py +++ b/backend/main.py @@ -18,12 +18,20 @@ router = APIRouter(prefix="/autojobs/api") # --- Plans & Pricing --- PLANS = { - "free": {"name": "Free", "applications": 5, "ai_customize": 5, "price": 0}, - "starter": {"name": "Starter", "applications": 50, "ai_customize": 50, "price": 9}, - "pro": {"name": "Pro", "applications": 100, "ai_customize": 100, "price": 39}, - "ultra": {"name": "Ultra", "applications": 200, "ai_customize": 200, "price": 69}, - "business": {"name": "Business", "applications": 500, "ai_customize": 500, "price": 119}, - "unlimited": {"name": "Unlimited", "applications": 99999, "ai_customize": 99999, "price": 319}, + "free": {"name": "Free", "applications": 5, "ai_customize": 5, "price": 0, "type": "jobseeker"}, + "starter": {"name": "Starter", "applications": 50, "ai_customize": 50, "price": 9, "type": "jobseeker"}, + "pro": {"name": "Pro", "applications": 100, "ai_customize": 100, "price": 39, "type": "jobseeker"}, + "ultra": {"name": "Ultra", "applications": 200, "ai_customize": 200, "price": 69, "type": "jobseeker"}, + "business": {"name": "Business", "applications": 500, "ai_customize": 500, "price": 119, "type": "jobseeker"}, + "unlimited": {"name": "Unlimited", "applications": 99999, "ai_customize": 99999, "price": 319, "type": "jobseeker"}, +} + +# Agency Plans (recruiting agencies - manage multiple clients) +AGENCY_PLANS = { + "agency_starter": {"name": "Agency Starter", "submissions": 1000, "clients": 10, "price": 555, "type": "agency"}, + "agency_growth": {"name": "Agency Growth", "submissions": 3000, "clients": 50, "price": 999, "type": "agency"}, + "agency_scale": {"name": "Agency Scale", "submissions": 5000, "clients": 150, "price": 1499, "type": "agency"}, + "agency_enterprise": {"name": "Agency Enterprise", "submissions": 10000, "clients": 500, "price": 2222, "type": "agency"}, } # --- Database --- @@ -76,6 +84,20 @@ def init_db(): ) """) + # Agency client profiles + c.execute(""" + CREATE TABLE IF NOT EXISTS client_profiles ( + id TEXT PRIMARY KEY, + agency_id TEXT NOT NULL, + client_name TEXT, + client_email TEXT, + profile_data TEXT DEFAULT '{}', + submission_count INTEGER DEFAULT 0, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (agency_id) REFERENCES users(id) + ) + """) + conn.commit() conn.close() @@ -111,8 +133,20 @@ def get_user_row(user_id: str): def check_plan_limit(user: dict, action: str, count: int = 1) -> dict: """Check if user has reached their plan limit. Returns {"allowed": bool, "remaining": int}""" plan_name = user.get("plan", "free") + + # Check if agency plan + if plan_name in AGENCY_PLANS: + plan = AGENCY_PLANS[plan_name] + if action == "submission": + used = user.get("monthly_count", 0) + limit = plan["submissions"] + else: + return {"allowed": True, "remaining": 99999} + remaining = max(0, limit - used) + return {"allowed": remaining >= count, "remaining": remaining, "limit": limit, "used": used, "type": "agency"} + + # Jobseeker plan plan = PLANS.get(plan_name, PLANS["free"]) - if action == "application": used = user.get("monthly_count", 0) limit = plan["applications"] @@ -123,12 +157,12 @@ def check_plan_limit(user: dict, action: str, count: int = 1) -> dict: return {"allowed": True, "remaining": 99999} remaining = max(0, limit - used) - return {"allowed": remaining >= count, "remaining": remaining, "limit": limit, "used": used} + return {"allowed": remaining >= count, "remaining": remaining, "limit": limit, "used": used, "type": "jobseeker"} def increment_usage(user_id: str, action: str): conn = sqlite3.connect(DB_PATH) c = conn.cursor() - if action == "application": + if action == "submission": c.execute("UPDATE users SET monthly_count = monthly_count + 1 WHERE id = ?", (user_id,)) elif action == "ai_customize": c.execute("UPDATE users SET ai_customize_count = ai_customize_count + 1 WHERE id = ?", (user_id,)) @@ -315,8 +349,11 @@ def get_profile(user=Depends(get_current_user)): @router.get("/plans") def list_plans(): - """List all available plans""" - return {"plans": PLANS} + """List all available plans (jobseeker + agency)""" + return { + "jobseeker_plans": PLANS, + "agency_plans": AGENCY_PLANS + } # --- Job Search --- @router.post("/jobs/search") @@ -504,17 +541,20 @@ def submit_application(app_data: ApplicationSubmit, user=Depends(get_current_use if not user_row: raise HTTPException(status_code=404, detail="User not found") - # Check plan limit - check = check_plan_limit(user_row, "application") + # Check plan limit (use "submission" for agency, "application" for jobseeker) + plan_name = user_row.get("plan", "free") + action = "submission" if plan_name in AGENCY_PLANS else "application" + check = check_plan_limit(user_row, action) if not check["allowed"]: raise HTTPException( status_code=402, detail={ - "error": "Application limit reached", - "plan": user_row["plan"], + "error": "Limit reached", + "plan": plan_name, "limit": check["limit"], "used": check["used"], - "message": f"You've applied to {check['used']} jobs this billing cycle. Upgrade your plan for more applications." + "type": check.get("type", "jobseeker"), + "message": f"You've used {check['used']} of your {check['limit']} submissions this billing cycle. Upgrade your plan for more." } ) @@ -541,7 +581,8 @@ def submit_application(app_data: ApplicationSubmit, user=Depends(get_current_use conn.close() # Increment usage - increment_usage(user["id"], "application") + action = "submission" if plan_name in AGENCY_PLANS else "application" + increment_usage(user["id"], action) return {"id": app_id, "status": "submitted"} @@ -648,6 +689,88 @@ def admin_applications(limit: int = 100): conn.close() return {"applications": [dict(r) for r in rows]} +# --- Agency Routes --- +@router.post("/agency/clients") +def create_client(client_data: dict, user=Depends(get_current_user)): + """Create a client profile under the agency account""" + plan_name = user.get("id") # We need to check plan from user dict + user_row = get_user_row(user["id"]) + if not user_row or user_row["plan"] not in AGENCY_PLANS: + raise HTTPException(status_code=403, detail="Agency plan required") + + plan = AGENCY_PLANS[user_row["plan"]] + # Check client limit + conn = sqlite3.connect(DB_PATH) + conn.row_factory = sqlite3.Row + c = conn.cursor() + c.execute("SELECT COUNT(*) as count FROM client_profiles WHERE agency_id = ?", (user["id"],)) + current_clients = c.fetchone()["count"] + + if current_clients >= plan["clients"]: + conn.close() + raise HTTPException(status_code=402, detail=f"Client limit reached. Your plan allows {plan['clients']} clients.") + + client_id = str(uuid.uuid4()) + c.execute(""" + INSERT INTO client_profiles (id, agency_id, client_name, client_email, profile_data) + VALUES (?, ?, ?, ?, ?) + """, ( + client_id, + user["id"], + client_data.get("name", ""), + client_data.get("email", ""), + json.dumps(client_data.get("profile", {})) + )) + conn.commit() + conn.close() + return {"client_id": client_id, "name": client_data.get("name", "")} + +@router.get("/agency/clients") +def list_clients(user=Depends(get_current_user)): + """List all clients under agency account""" + user_row = get_user_row(user["id"]) + if not user_row or user_row["plan"] not in AGENCY_PLANS: + raise HTTPException(status_code=403, detail="Agency plan required") + + conn = sqlite3.connect(DB_PATH) + conn.row_factory = sqlite3.Row + c = conn.cursor() + c.execute("SELECT * FROM client_profiles WHERE agency_id = ? ORDER BY created_at DESC", (user["id"],)) + rows = c.fetchall() + conn.close() + return {"clients": [dict(r) for r in rows]} + +@router.get("/agency/usage") +def agency_usage(user=Depends(get_current_user)): + """Get agency submission usage""" + user_row = get_user_row(user["id"]) + if not user_row or user_row["plan"] not in AGENCY_PLANS: + raise HTTPException(status_code=403, detail="Agency plan required") + + plan = AGENCY_PLANS[user_row["plan"]] + return { + "plan": user_row["plan"], + "submissions_limit": plan["submissions"], + "submissions_used": user_row["monthly_count"], + "submissions_remaining": max(0, plan["submissions"] - user_row["monthly_count"]), + "clients_limit": plan["clients"], + "clients_used": len([]), # Will be populated from DB + } + +@router.delete("/agency/clients/{client_id}") +def delete_client(client_id: str, user=Depends(get_current_user)): + """Delete a client profile""" + user_row = get_user_row(user["id"]) + if not user_row or user_row["plan"] not in AGENCY_PLANS: + raise HTTPException(status_code=403, detail="Agency plan required") + + conn = sqlite3.connect(DB_PATH) + c = conn.cursor() + c.execute("DELETE FROM client_profiles WHERE id = ? AND agency_id = ?", (client_id, user["id"])) + conn.commit() + conn.close() + return {"deleted": True} + @router.post("/admin/reset-usage") def admin_reset_usage(user_id: str = None): """Admin: Reset usage counts (monthly billing cycle reset)""" diff --git a/frontend/app/page.tsx b/frontend/app/page.tsx index 91ef93c..b3c77b6 100644 --- a/frontend/app/page.tsx +++ b/frontend/app/page.tsx @@ -1,56 +1,103 @@ import Link from "next/link" -const plans = [ +const jobseekerPlans = [ { name: "Free", price: "$0", period: "forever", apps: "5", - ai: "5", - badge: "Start Here", + badge: "", highlight: false, - features: ["5 AI job applications/month", "5 AI resume customizations/month", "Application tracker", "Add your own API keys", "Multi-source search"], - cta: "Get Started Free", + features: ["5 AI job applications/month", "5 AI resume customizations/month", "Application tracker", "Add your own API keys"], + cta: "Get Started", ctaStyle: "bg-white/10 hover:bg-white/20" }, { name: "Starter", price: "$9", - period: "/month", + period: "/mo", apps: "50", - ai: "50", badge: "", highlight: false, - features: ["50 AI job applications/month", "50 AI resume customizations/month", "Application tracker", "Add your own API keys", "Email notifications"], + features: ["50 AI job applications/month", "50 AI resume customizations/month", "Application tracker", "Add your own API keys"], cta: "Start Applying", ctaStyle: "bg-blue-500 hover:bg-blue-600" }, { name: "Pro", price: "$39", - period: "/month", + period: "/mo", apps: "100", - ai: "100", - badge: "Most Popular", + badge: "Popular", highlight: true, - features: ["100 AI job applications/month", "100 AI resume customizations/month", "Application tracker", "Add your own API keys", "Email notifications", "Interview prep tips"], + features: ["100 AI job applications/month", "100 AI resume customizations/month", "Application tracker", "Add your own API keys", "Email notifications"], cta: "Go Pro", ctaStyle: "bg-blue-500 hover:bg-blue-600" }, { name: "Ultra", price: "$69", - period: "/month", + period: "/mo", apps: "200", - ai: "200", badge: "", highlight: false, - features: ["200 AI job applications/month", "200 AI resume customizations/month", "Priority processing", "Add your own API keys", "Email + SMS notifications"], + features: ["200 AI job applications/month", "200 AI resume customizations/month", "Priority processing", "Email + SMS notifications"], cta: "Go Ultra", ctaStyle: "bg-slate-600 hover:bg-slate-500" }, ] +const agencyPlans = [ + { + name: "Starter", + price: "$555", + period: "/mo", + submissions: "1,000", + clients: "10", + badge: "", + highlight: false, + features: ["1,000 job submissions/month", "Up to 10 client profiles", "AI resume tailoring", "White-label dashboard", "Priority support"], + cta: "Start Agency", + ctaStyle: "bg-purple-600 hover:bg-purple-500" + }, + { + name: "Growth", + price: "$999", + period: "/mo", + submissions: "3,000", + clients: "50", + badge: "", + highlight: true, + features: ["3,000 job submissions/month", "Up to 50 client profiles", "AI resume tailoring", "White-label dashboard", "Priority support"], + cta: "Grow Agency", + ctaStyle: "bg-purple-600 hover:bg-purple-500" + }, + { + name: "Scale", + price: "$1,499", + period: "/mo", + submissions: "5,000", + clients: "150", + badge: "", + highlight: false, + features: ["5,000 job submissions/month", "Up to 150 client profiles", "AI resume tailoring", "White-label dashboard", "Dedicated account manager"], + cta: "Scale Up", + ctaStyle: "bg-slate-600 hover:bg-slate-500" + }, + { + name: "Enterprise", + price: "$2,222", + period: "/mo", + submissions: "10,000", + clients: "500", + badge: "", + highlight: false, + features: ["10,000 job submissions/month", "Up to 500 client profiles", "AI resume tailoring", "White-label dashboard", "Dedicated account manager", "Custom integrations"], + cta: "Get Enterprise", + ctaStyle: "bg-slate-600 hover:bg-slate-500" + }, +] + export default function LandingPage() { return (
Three steps to never manually applying to a job again.
{f.desc}
-Pick the plan that fits your job search. No hidden fees.
+Pick the plan that fits your job search.
-Need more? Business (500/mo) = $119 | Unlimited = $319/month
+ {/* Agency Pricing */} +Run job applications for your entire client roster. Each plan includes client profile management and submission limits.
+ +No unlimited agency plan — all submissions are capped to prevent abuse.