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
« prev ^ index » next coverage.py v7.13.5, created at 2026-05-06 23:45 +0000
1"""Payment data models with legacy shape compatibility."""
3from dataclasses import dataclass
4from enum import Enum
5from typing import Any
8class PlanType(Enum):
9 """Plan types matching legacy frontend expectations."""
11 PREPAID = "prepaid"
12 POSTPAID = "postpaid"
13 INTRO = "intro"
16class SubscriptionStatus(Enum):
17 """Subscription statuses."""
19 ACTIVE = "active"
20 PAUSED = "paused"
21 CANCELLED = "cancelled"
22 PAST_DUE = "past_due"
25@dataclass
26class Plan:
27 """
28 Plan model matching legacy frontend data structure.
29 Maps from Stripe Price/Product to legacy fields.
30 """
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
41 # Additional fields for internal use
42 stripe_price_id: str | None = None
43 stripe_product_id: str | None = None
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 }
58 # Add variable_amount flag for VARIABLE product
59 if self.plan_name == "VARIABLE":
60 result["variable_amount"] = True
62 return result
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()
76 metadata = price.get("metadata", {})
78 # Determine plan type
79 if price.get("recurring"):
80 plan_type = "postpaid"
81 else:
82 plan_type = metadata.get("plan_type", "prepaid")
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 = ""
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 )