Coverage for src/alprina_cli/billing.py: 16%
86 statements
« prev ^ index » next coverage.py v7.11.3, created at 2025-11-14 11:27 +0100
« prev ^ index » next coverage.py v7.11.3, created at 2025-11-14 11:27 +0100
1"""
2Billing integration module for Alprina CLI.
3Handles Stripe integration and usage metering.
4"""
6import os
7from typing import Optional
8import httpx
9from rich.console import Console
10from rich.panel import Panel
11from rich.table import Table
13from .auth import is_authenticated, get_auth_headers, load_token
15console = Console()
18def get_backend_url() -> str:
19 """Get backend URL from environment or use default."""
20 return os.getenv("ALPRINA_BACKEND", "https://api.alprina.com/v1")
23def billing_status_command():
24 """Display billing status and usage information."""
25 if not is_authenticated():
26 console.print("[red]Please login first: alprina auth login[/red]")
27 return
29 console.print(Panel("💳 Fetching billing information...", title="Billing Status"))
31 try:
32 backend_url = get_backend_url()
33 headers = get_auth_headers()
35 response = httpx.get(
36 f"{backend_url}/billing/usage",
37 headers=headers,
38 timeout=10.0
39 )
41 if response.status_code == 200:
42 data = response.json()
43 _display_billing_info(data)
44 else:
45 console.print(f"[red]Failed to fetch billing info: {response.text}[/red]")
47 except httpx.ConnectError:
48 console.print("[yellow]Could not connect to backend. Displaying local info.[/yellow]")
49 _display_local_billing_info()
50 except Exception as e:
51 console.print(f"[red]Error: {e}[/red]")
54def _display_billing_info(data: dict):
55 """Display billing information from API response."""
56 user = data.get("user", {})
57 usage = data.get("usage", {})
58 plan = data.get("plan", {})
60 # Create info panel
61 info = f"""[bold]Plan:[/bold] {plan.get('name', 'Free')}
62[bold]Status:[/bold] {plan.get('status', 'Active')}
63"""
65 if plan.get('billing_cycle'):
66 info += f"[bold]Billing Cycle:[/bold] {plan['billing_cycle']}\n"
68 console.print(Panel(info, title="Subscription Info"))
70 # Create usage table
71 table = Table(title="Usage Statistics", show_header=True, header_style="bold cyan")
72 table.add_column("Metric", style="bold")
73 table.add_column("Used", justify="right")
74 table.add_column("Limit", justify="right")
75 table.add_column("Status", justify="center")
77 scans_used = usage.get("scans_used", 0)
78 scans_limit = plan.get("scans_limit", 10)
79 scans_pct = (scans_used / scans_limit * 100) if scans_limit > 0 else 0
81 status_color = "green" if scans_pct < 80 else "yellow" if scans_pct < 100 else "red"
83 table.add_row(
84 "Scans (today)",
85 str(scans_used),
86 str(scans_limit),
87 f"[{status_color}]{scans_pct:.0f}%[/{status_color}]"
88 )
90 console.print(table)
92 # Show upgrade message if on free plan
93 if plan.get('name') == 'Free':
94 console.print("\n[cyan]💡 Upgrade to Pro for unlimited scans and advanced features![/cyan]")
95 console.print("[dim]Visit https://alprina.ai/pricing[/dim]")
98def _display_local_billing_info():
99 """Display billing info from local token when offline."""
100 auth_data = load_token()
102 if not auth_data:
103 console.print("[red]Not authenticated[/red]")
104 return
106 user = auth_data.get("user", {})
107 plan = user.get("plan", "free")
109 console.print(Panel(
110 f"[bold]Plan:[/bold] {plan.title()}\n"
111 f"[yellow]Offline mode - showing cached information[/yellow]",
112 title="Billing Status"
113 ))
116def check_scan_quota() -> bool:
117 """
118 Check if user has remaining scan quota.
120 Returns:
121 True if user can run a scan, False otherwise
122 """
123 if not is_authenticated():
124 return False
126 try:
127 backend_url = get_backend_url()
128 headers = get_auth_headers()
130 response = httpx.get(
131 f"{backend_url}/billing/usage",
132 headers=headers,
133 timeout=5.0
134 )
136 if response.status_code == 200:
137 data = response.json()
138 usage = data.get("usage", {})
139 plan = data.get("plan", {})
141 scans_used = usage.get("scans_used", 0)
142 scans_limit = plan.get("scans_limit", 10)
144 if scans_used >= scans_limit:
145 console.print(f"[red]Scan quota exceeded ({scans_used}/{scans_limit})[/red]")
146 console.print("[yellow]Upgrade your plan to continue scanning[/yellow]")
147 return False
149 return True
151 except Exception:
152 # Allow scans if backend is unreachable
153 return True
155 return True
158def increment_usage(scan_type: str = "scan"):
159 """
160 Increment usage counter after a successful scan.
162 Args:
163 scan_type: Type of scan performed
164 """
165 try:
166 backend_url = get_backend_url()
167 headers = get_auth_headers()
169 httpx.post(
170 f"{backend_url}/billing/usage/increment",
171 headers=headers,
172 json={"type": scan_type},
173 timeout=5.0
174 )
176 except Exception:
177 # Silently fail if backend is unreachable
178 pass