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

1""" 

2Subscription Management - /v1/billing/ 

3Handles subscription operations (cancel, update, etc.) 

4""" 

5 

6from fastapi import APIRouter, HTTPException, Depends 

7from pydantic import BaseModel 

8from typing import Optional 

9from loguru import logger 

10 

11from ..services.polar_service import polar_service 

12from ..services.neon_service import neon_service 

13 

14router = APIRouter() 

15 

16 

17class CancelSubscriptionRequest(BaseModel): 

18 """Request to cancel a subscription.""" 

19 subscription_id: str 

20 

21 

22class CancelSubscriptionResponse(BaseModel): 

23 """Response after canceling subscription.""" 

24 status: str 

25 message: str 

26 subscription_id: str 

27 ends_at: Optional[str] = None 

28 

29 

30@router.post("/billing/cancel-subscription", response_model=CancelSubscriptionResponse) 

31async def cancel_subscription(request: CancelSubscriptionRequest): 

32 """ 

33 Cancel a user's subscription. 

34 

35 The subscription will remain active until the end of the current billing period. 

36 

37 **Request Body:** 

38 ```json 

39 { 

40 "subscription_id": "sub_123..." 

41 } 

42 ``` 

43 

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 

55 

56 if not subscription_id: 

57 raise HTTPException( 

58 status_code=400, 

59 detail="Subscription ID is required" 

60 ) 

61 

62 logger.info(f"Canceling subscription: {subscription_id}") 

63 

64 try: 

65 # Cancel via Polar API 

66 result = await polar_service.cancel_subscription(subscription_id) 

67 

68 if not result: 

69 raise HTTPException( 

70 status_code=404, 

71 detail="Subscription not found" 

72 ) 

73 

74 # Get updated subscription info 

75 subscription = result.get("subscription", {}) 

76 ends_at = subscription.get("current_period_end") 

77 

78 logger.info(f"Successfully canceled subscription {subscription_id}") 

79 

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 ) 

86 

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 ) 

95 

96 

97@router.get("/billing/subscription/{subscription_id}") 

98async def get_subscription(subscription_id: str): 

99 """ 

100 Get subscription details. 

101 

102 Returns current subscription status, billing period, and usage info. 

103 """ 

104 try: 

105 subscription = await polar_service.get_subscription(subscription_id) 

106 

107 if not subscription: 

108 raise HTTPException( 

109 status_code=404, 

110 detail="Subscription not found" 

111 ) 

112 

113 return { 

114 "status": "success", 

115 "subscription": subscription 

116 } 

117 

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 )