Coverage for app\services\billing_service.py: 22%
49 statements
« prev ^ index » next coverage.py v7.3.2, created at 2026-01-15 13:39 +0300
« prev ^ index » next coverage.py v7.3.2, created at 2026-01-15 13:39 +0300
1from sqlalchemy.orm import Session
2from sqlalchemy import func
3from .. import db as models
4from datetime import datetime, UTC
5from typing import Optional, Dict, Any
7class BillingService:
8 def __init__(self, db: Session):
9 self.db = db
11 def get_active_subscription(self, school_id: str) -> Optional[models.Subscription]:
12 return self.db.query(models.Subscription).filter(
13 models.Subscription.school_id == school_id,
14 models.Subscription.status == "active"
15 ).first()
17 def check_limit(self, school_id: str, resource_key: str, current_value: Optional[float] = None, requested_amount: float = 1.0) -> bool:
18 """
19 Check if a school has enough quota for a resource.
21 Args:
22 school_id: ID of the school
23 resource_key: Key in the plan's 'limits' JSON (e.g., 'max_students', 'storage_gb')
24 current_value: If provided, this value is used as current usage.
25 If None, the service tries to fetch it from SchoolMetric or UsageRecord.
26 requested_amount: Amount to add/check.
28 Returns:
29 True if allowed, False if limit exceeded.
30 """
31 subscription = self.get_active_subscription(school_id)
32 if not subscription:
33 # No active subscription usually means strict limits or no access
34 # For now, let's assume default strict limits (0) if no plan
35 return False
37 plan = subscription.plan
38 if not plan or not plan.limits:
39 return True # No limits defined means unlimited? Or 0? Assuming unlimited for now if field missing.
41 limits = plan.limits
42 if resource_key not in limits:
43 return True # Limit not enforced for this resource
45 limit_value = limits[resource_key]
46 if limit_value == -1: # Convention for unlimited
47 return True
49 # Determine current usage if not provided
50 if current_value is None:
51 current_value = self._get_current_usage(school_id, resource_key)
53 return (current_value + requested_amount) <= limit_value
55 def _get_current_usage(self, school_id: str, resource_key: str) -> float:
56 """
57 Determine current usage based on resource key.
58 """
59 if resource_key == "max_students" or resource_key == "students":
60 # Try to get from SchoolMetric
61 metric = self.db.query(models.SchoolMetric).filter(
62 models.SchoolMetric.school_id == school_id
63 ).order_by(models.SchoolMetric.last_seen_at.desc()).first()
64 if metric:
65 return float(metric.active_users)
66 return 0.0
68 elif resource_key == "storage_gb":
69 # Try to get from SchoolMetric
70 metric = self.db.query(models.SchoolMetric).filter(
71 models.SchoolMetric.school_id == school_id
72 ).order_by(models.SchoolMetric.last_seen_at.desc()).first()
73 if metric:
74 # disk_usage is likely in bytes or MB, let's assume MB and convert to GB?
75 # Schema says Integer. Let's assume it matches the limit unit for now.
76 return float(metric.disk_usage)
77 return 0.0
79 # Fallback to UsageRecord for cumulative metrics
80 # (This might need summing up usage for the current billing period)
81 # For simplicity, returning 0 if unknown
82 return 0.0
84 def get_limits_status(self, school_id: str) -> Dict[str, Any]:
85 """
86 Return a summary of limits and usage for a school.
87 """
88 subscription = self.get_active_subscription(school_id)
89 if not subscription:
90 return {"status": "no_subscription"}
92 plan = subscription.plan
93 limits = plan.limits or {}
95 status = {}
96 for key, limit in limits.items():
97 usage = self._get_current_usage(school_id, key)
98 status[key] = {
99 "limit": limit,
100 "usage": usage,
101 "remaining": limit - usage if limit != -1 else "unlimited"
102 }
104 return status