1
2import stripe
3from sqlalchemy import select, func as sa_func
4from sqlalchemy.ext.asyncio import AsyncSession
5from datetime import datetime
6from uuid import uuid4
7from app.models import Plan, Subscription, UsageEvent, SubscriptionStatus
8from app.config import settings
9
10stripe.api_key = settings.stripe_secret_key
11
12class BillingService:
13 def __init__(self, db: AsyncSession):
14 self.db = db
15
16 async def create_subscription(
17 self, customer_id: str, plan_slug: str,
18 quantity: int, stripe_customer_id: str,
19 ) -> Subscription:
20 result = await self.db.execute(
21 select(Plan).where(Plan.slug == plan_slug, Plan.active == True)
22 )
23 plan = result.scalar_one()
24
25 stripe_sub = stripe.Subscription.create(
26 customer=stripe_customer_id,
27 items=[{"price": plan.stripe_price_id, "quantity": quantity}],
28 )
29
30 sub = Subscription(
31 id=str(uuid4()),
32 customer_id=customer_id,
33 plan_id=plan.id,
34 stripe_subscription_id=stripe_sub.id,
35 stripe_customer_id=stripe_customer_id,
36 quantity=quantity,
37 current_period_start=datetime.fromtimestamp(stripe_sub.current_period_start),
38 current_period_end=datetime.fromtimestamp(stripe_sub.current_period_end),
39 )
40 self.db.add(sub)
41 await self.db.commit()
42 await self.db.refresh(sub)
43 return sub
44
45 async def change_plan(self, subscription_id: str, new_plan_slug: str):
46 sub = await self.db.get(Subscription, subscription_id)
47 result = await self.db.execute(
48 select(Plan).where(Plan.slug == new_plan_slug)
49 )
50 new_plan = result.scalar_one()
51
52 items = stripe.SubscriptionItem.list(subscription=sub.stripe_subscription_id)
53
54 stripe.Subscription.modify(
55 sub.stripe_subscription_id,
56 items=[{
57 "id": items.data[0].id,
58 "price": new_plan.stripe_price_id,
59 }],
60 proration_behavior="create_prorations",
61 )
62
63 sub.plan_id = new_plan.id
64 await self.db.commit()
65 return sub
66
67 async def update_seats(self, subscription_id: str, new_quantity: int):
68 sub = await self.db.get(Subscription, subscription_id)
69
70 items = stripe.SubscriptionItem.list(subscription=sub.stripe_subscription_id)
71
72 stripe.Subscription.modify(
73 sub.stripe_subscription_id,
74 items=[{
75 "id": items.data[0].id,
76 "quantity": new_quantity,
77 }],
78 proration_behavior="create_prorations",
79 )
80
81 sub.quantity = new_quantity
82 await self.db.commit()
83 return sub
84
85 async def cancel_subscription(self, subscription_id: str, immediately: bool = False):
86 sub = await self.db.get(Subscription, subscription_id)
87
88 if immediately:
89 stripe.Subscription.cancel(sub.stripe_subscription_id)
90 sub.status = SubscriptionStatus.cancelled
91 sub.cancelled_at = datetime.utcnow()
92 else:
93 stripe.Subscription.modify(
94 sub.stripe_subscription_id,
95 cancel_at_period_end=True,
96 )
97 sub.cancel_at_period_end = True
98
99 await self.db.commit()
100 return sub
101
102 async def record_usage(
103 self, subscription_id: str, metric_id: str,
104 quantity: int, idempotency_key: str,
105 ):
106 existing = await self.db.execute(
107 select(UsageEvent).where(UsageEvent.idempotency_key == idempotency_key)
108 )
109 if existing.scalar_one_or_none():
110 return
111
112 event = UsageEvent(
113 id=str(uuid4()),
114 idempotency_key=idempotency_key,
115 subscription_id=subscription_id,
116 metric_id=metric_id,
117 quantity=quantity,
118 timestamp=datetime.utcnow(),
119 )
120 self.db.add(event)
121 await self.db.commit()
122
123 async def get_usage(
124 self, subscription_id: str, metric_id: str,
125 period_start: datetime, period_end: datetime,
126 ) -> int:
127 result = await self.db.execute(
128 select(sa_func.coalesce(sa_func.sum(UsageEvent.quantity), 0))
129 .where(
130 UsageEvent.subscription_id == subscription_id,
131 UsageEvent.metric_id == metric_id,
132 UsageEvent.timestamp >= period_start,
133 UsageEvent.timestamp < period_end,
134 )
135 )
136 return int(result.scalar())
137
138 async def check_feature(self, customer_id: str, feature: str) -> bool:
139 result = await self.db.execute(
140 select(Subscription)
141 .join(Plan)
142 .where(
143 Subscription.customer_id == customer_id,
144 Subscription.status == SubscriptionStatus.active,
145 )
146 )
147 sub = result.scalar_one_or_none()
148 if not sub:
149 return False
150 return feature in (sub.plan.features or [])
151