Coverage for src / ai_lls_lib / payment / models.py: 100%

53 statements  

« prev     ^ index     » next       coverage.py v7.13.5, created at 2026-05-06 23:45 +0000

1"""Payment data models with legacy shape compatibility.""" 

2 

3from dataclasses import dataclass 

4from enum import Enum 

5from typing import Any 

6 

7 

8class PlanType(Enum): 

9 """Plan types matching legacy frontend expectations.""" 

10 

11 PREPAID = "prepaid" 

12 POSTPAID = "postpaid" 

13 INTRO = "intro" 

14 

15 

16class SubscriptionStatus(Enum): 

17 """Subscription statuses.""" 

18 

19 ACTIVE = "active" 

20 PAUSED = "paused" 

21 CANCELLED = "cancelled" 

22 PAST_DUE = "past_due" 

23 

24 

25@dataclass 

26class Plan: 

27 """ 

28 Plan model matching legacy frontend data structure. 

29 Maps from Stripe Price/Product to legacy fields. 

30 """ 

31 

32 plan_reference: str # Stripe price ID or legacy reference 

33 plan_type: str # prepaid, postpaid, intro 

34 plan_name: str # STANDARD, POWER, ELITE, UNLIMITED 

35 plan_subtitle: str 

36 plan_amount: float # Price in USD 

37 plan_credits: int | None # Number of credits or None for unlimited 

38 plan_credits_text: str # Display text like "5,000 credits" 

39 percent_off: str # Discount percentage display text 

40 

41 # Additional fields for internal use 

42 stripe_price_id: str | None = None 

43 stripe_product_id: str | None = None 

44 

45 def to_dict(self) -> dict[str, Any]: 

46 """Convert to dictionary for JSON serialization.""" 

47 result = { 

48 "plan_reference": self.plan_reference, 

49 "plan_type": self.plan_type, 

50 "plan_name": self.plan_name, 

51 "plan_subtitle": self.plan_subtitle, 

52 "plan_amount": self.plan_amount, 

53 "plan_credits": self.plan_credits, 

54 "plan_credits_text": self.plan_credits_text, 

55 "percent_off": self.percent_off, 

56 } 

57 

58 # Add variable_amount flag for VARIABLE product 

59 if self.plan_name == "VARIABLE": 

60 result["variable_amount"] = True 

61 

62 return result 

63 

64 @classmethod 

65 def from_stripe_price(cls, price: dict[str, Any], product: dict[str, Any]) -> "Plan": 

66 """ 

67 Create Plan from Stripe Price and Product objects. 

68 Maps Stripe metadata to legacy fields. 

69 """ 

70 # Convert StripeObject to dict if needed (Stripe v15 compatibility) 

71 if hasattr(price, "to_dict"): 

72 price = price.to_dict() 

73 if hasattr(product, "to_dict"): 

74 product = product.to_dict() 

75 

76 metadata = price.get("metadata", {}) 

77 

78 # Determine plan type 

79 if price.get("recurring"): 

80 plan_type = "postpaid" 

81 else: 

82 plan_type = metadata.get("plan_type", "prepaid") 

83 

84 # Extract credits 

85 credits_str = metadata.get("credits", "") 

86 if credits_str.lower() == "unlimited": 

87 plan_credits = None 

88 plan_credits_text = "Unlimited" 

89 elif credits_str: 

90 try: 

91 plan_credits = int(credits_str) 

92 plan_credits_text = f"{plan_credits:,} credits" 

93 except ValueError: 

94 plan_credits = None 

95 plan_credits_text = credits_str 

96 else: 

97 plan_credits = None 

98 plan_credits_text = "" 

99 

100 return cls( 

101 plan_reference=metadata.get("plan_reference", price["id"]), 

102 plan_type=plan_type, 

103 plan_name=metadata.get("tier", product.get("name", "")).upper(), 

104 plan_subtitle=metadata.get("plan_subtitle", product.get("description", "")), 

105 plan_amount=price["unit_amount"] / 100.0, # Convert cents to dollars 

106 plan_credits=plan_credits, 

107 plan_credits_text=metadata.get("plan_credits_text", plan_credits_text), 

108 percent_off=metadata.get("percent_off", ""), 

109 stripe_price_id=price["id"], 

110 stripe_product_id=product["id"], 

111 )