Coverage for src/alprina_cli/api/routes/subscription.py: 34%
41 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"""
2Subscription Management - /v1/billing/
3Handles subscription operations (cancel, update, etc.)
4"""
6from fastapi import APIRouter, HTTPException, Depends
7from pydantic import BaseModel
8from typing import Optional
9from loguru import logger
11from ..services.polar_service import polar_service
12from ..services.neon_service import neon_service
14router = APIRouter()
17class CancelSubscriptionRequest(BaseModel):
18 """Request to cancel a subscription."""
19 subscription_id: str
22class CancelSubscriptionResponse(BaseModel):
23 """Response after canceling subscription."""
24 status: str
25 message: str
26 subscription_id: str
27 ends_at: Optional[str] = None
30@router.post("/billing/cancel-subscription", response_model=CancelSubscriptionResponse)
31async def cancel_subscription(request: CancelSubscriptionRequest):
32 """
33 Cancel a user's subscription.
35 The subscription will remain active until the end of the current billing period.
37 **Request Body:**
38 ```json
39 {
40 "subscription_id": "sub_123..."
41 }
42 ```
44 **Response:**
45 ```json
46 {
47 "status": "canceled",
48 "message": "Subscription will end on 2025-11-12",
49 "subscription_id": "sub_123...",
50 "ends_at": "2025-11-12T13:15:54Z"
51 }
52 ```
53 """
54 subscription_id = request.subscription_id
56 if not subscription_id:
57 raise HTTPException(
58 status_code=400,
59 detail="Subscription ID is required"
60 )
62 logger.info(f"Canceling subscription: {subscription_id}")
64 try:
65 # Cancel via Polar API
66 result = await polar_service.cancel_subscription(subscription_id)
68 if not result:
69 raise HTTPException(
70 status_code=404,
71 detail="Subscription not found"
72 )
74 # Get updated subscription info
75 subscription = result.get("subscription", {})
76 ends_at = subscription.get("current_period_end")
78 logger.info(f"Successfully canceled subscription {subscription_id}")
80 return CancelSubscriptionResponse(
81 status="canceled",
82 message=f"Subscription will remain active until {ends_at}",
83 subscription_id=subscription_id,
84 ends_at=ends_at
85 )
87 except HTTPException:
88 raise
89 except Exception as e:
90 logger.error(f"Failed to cancel subscription {subscription_id}: {e}")
91 raise HTTPException(
92 status_code=500,
93 detail=f"Failed to cancel subscription: {str(e)}"
94 )
97@router.get("/billing/subscription/{subscription_id}")
98async def get_subscription(subscription_id: str):
99 """
100 Get subscription details.
102 Returns current subscription status, billing period, and usage info.
103 """
104 try:
105 subscription = await polar_service.get_subscription(subscription_id)
107 if not subscription:
108 raise HTTPException(
109 status_code=404,
110 detail="Subscription not found"
111 )
113 return {
114 "status": "success",
115 "subscription": subscription
116 }
118 except HTTPException:
119 raise
120 except Exception as e:
121 logger.error(f"Failed to fetch subscription {subscription_id}: {e}")
122 raise HTTPException(
123 status_code=500,
124 detail=f"Failed to fetch subscription: {str(e)}"
125 )