chore: reformat code style with consistent quotes, trailing commas, and spacing across auth, billing, and models

This commit is contained in:
2025-11-13 22:22:05 +00:00
parent aac0798305
commit 69f3161eec
34 changed files with 1851 additions and 875 deletions
+37 -20
View File
@@ -2,14 +2,17 @@ from datetime import datetime, date, timedelta, timezone
from decimal import Decimal
from typing import Optional
from calendar import monthrange
from .models import Invoice, InvoiceLineItem, PricingConfig, UsageAggregate, UserSubscription
from .models import Invoice, InvoiceLineItem, PricingConfig, UserSubscription
from .usage_tracker import UsageTracker
from .stripe_client import StripeClient
from ..models import User
class InvoiceGenerator:
@staticmethod
async def generate_monthly_invoice(user: User, year: int, month: int) -> Optional[Invoice]:
async def generate_monthly_invoice(
user: User, year: int, month: int
) -> Optional[Invoice]:
period_start = date(year, month, 1)
days_in_month = monthrange(year, month)[1]
period_end = date(year, month, days_in_month)
@@ -19,19 +22,24 @@ class InvoiceGenerator:
pricing = await PricingConfig.all()
pricing_dict = {p.config_key: p.config_value for p in pricing}
storage_price_per_gb = pricing_dict.get('storage_per_gb_month', Decimal('0.0045'))
bandwidth_price_per_gb = pricing_dict.get('bandwidth_egress_per_gb', Decimal('0.009'))
free_storage_gb = pricing_dict.get('free_tier_storage_gb', Decimal('15'))
free_bandwidth_gb = pricing_dict.get('free_tier_bandwidth_gb', Decimal('15'))
tax_rate = pricing_dict.get('tax_rate_default', Decimal('0'))
storage_price_per_gb = pricing_dict.get(
"storage_per_gb_month", Decimal("0.0045")
)
bandwidth_price_per_gb = pricing_dict.get(
"bandwidth_egress_per_gb", Decimal("0.009")
)
free_storage_gb = pricing_dict.get("free_tier_storage_gb", Decimal("15"))
free_bandwidth_gb = pricing_dict.get("free_tier_bandwidth_gb", Decimal("15"))
tax_rate = pricing_dict.get("tax_rate_default", Decimal("0"))
storage_gb = Decimal(str(usage['storage_gb_avg']))
bandwidth_gb = Decimal(str(usage['bandwidth_down_gb']))
storage_gb = Decimal(str(usage["storage_gb_avg"]))
bandwidth_gb = Decimal(str(usage["bandwidth_down_gb"]))
billable_storage = max(Decimal('0'), storage_gb - free_storage_gb)
billable_bandwidth = max(Decimal('0'), bandwidth_gb - free_bandwidth_gb)
billable_storage = max(Decimal("0"), storage_gb - free_storage_gb)
billable_bandwidth = max(Decimal("0"), bandwidth_gb - free_bandwidth_gb)
import math
billable_storage_rounded = Decimal(math.ceil(float(billable_storage)))
billable_bandwidth_rounded = Decimal(math.ceil(float(billable_bandwidth)))
@@ -65,9 +73,9 @@ class InvoiceGenerator:
"usage": usage,
"pricing": {
"storage_per_gb": float(storage_price_per_gb),
"bandwidth_per_gb": float(bandwidth_price_per_gb)
}
}
"bandwidth_per_gb": float(bandwidth_price_per_gb),
},
},
)
if billable_storage_rounded > 0:
@@ -78,7 +86,10 @@ class InvoiceGenerator:
unit_price=storage_price_per_gb,
amount=storage_cost,
item_type="storage",
metadata={"avg_gb": float(storage_gb), "free_gb": float(free_storage_gb)}
metadata={
"avg_gb": float(storage_gb),
"free_gb": float(free_storage_gb),
},
)
if billable_bandwidth_rounded > 0:
@@ -89,7 +100,10 @@ class InvoiceGenerator:
unit_price=bandwidth_price_per_gb,
amount=bandwidth_cost,
item_type="bandwidth",
metadata={"total_gb": float(bandwidth_gb), "free_gb": float(free_bandwidth_gb)}
metadata={
"total_gb": float(bandwidth_gb),
"free_gb": float(free_bandwidth_gb),
},
)
if subscription and subscription.stripe_customer_id:
@@ -100,7 +114,7 @@ class InvoiceGenerator:
"amount": item.amount,
"currency": "usd",
"description": item.description,
"metadata": item.metadata or {}
"metadata": item.metadata or {},
}
for item in line_items
]
@@ -109,7 +123,7 @@ class InvoiceGenerator:
customer_id=subscription.stripe_customer_id,
description=f"MyWebdav Usage Invoice for {period_start.strftime('%B %Y')}",
line_items=stripe_line_items,
metadata={"mywebdav_invoice_id": str(invoice.id)}
metadata={"mywebdav_invoice_id": str(invoice.id)},
)
invoice.stripe_invoice_id = stripe_invoice.id
@@ -135,8 +149,11 @@ class InvoiceGenerator:
# Send invoice email
from ..mail import queue_email
line_items = await invoice.line_items.all()
items_text = "\n".join([f"- {item.description}: ${item.amount}" for item in line_items])
items_text = "\n".join(
[f"- {item.description}: ${item.amount}" for item in line_items]
)
body = f"""Dear {invoice.user.username},
Your invoice {invoice.invoice_number} for the period {invoice.period_start} to {invoice.period_end} is now available.
@@ -174,7 +191,7 @@ The MyWebdav Team
to_email=invoice.user.email,
subject=f"Your MyWebdav Invoice {invoice.invoice_number}",
body=body,
html=html
html=html,
)
return invoice
+31 -17
View File
@@ -1,8 +1,8 @@
from tortoise import fields, models
from decimal import Decimal
class SubscriptionPlan(models.Model):
id = fields.IntField(pk=True)
id = fields.IntField(primary_key=True)
name = fields.CharField(max_length=100, unique=True)
display_name = fields.CharField(max_length=255)
description = fields.TextField(null=True)
@@ -18,10 +18,13 @@ class SubscriptionPlan(models.Model):
class Meta:
table = "subscription_plans"
class UserSubscription(models.Model):
id = fields.IntField(pk=True)
id = fields.IntField(primary_key=True)
user = fields.ForeignKeyField("models.User", related_name="subscription")
plan = fields.ForeignKeyField("billing.SubscriptionPlan", related_name="subscriptions", null=True)
plan = fields.ForeignKeyField(
"billing.SubscriptionPlan", related_name="subscriptions", null=True
)
billing_type = fields.CharField(max_length=20, default="pay_as_you_go")
stripe_customer_id = fields.CharField(max_length=255, unique=True, null=True)
stripe_subscription_id = fields.CharField(max_length=255, unique=True, null=True)
@@ -35,14 +38,15 @@ class UserSubscription(models.Model):
class Meta:
table = "user_subscriptions"
class UsageRecord(models.Model):
id = fields.BigIntField(pk=True)
id = fields.BigIntField(primary_key=True)
user = fields.ForeignKeyField("models.User", related_name="usage_records")
record_type = fields.CharField(max_length=50, index=True)
record_type = fields.CharField(max_length=50, db_index=True)
amount_bytes = fields.BigIntField()
resource_type = fields.CharField(max_length=50, null=True)
resource_id = fields.IntField(null=True)
timestamp = fields.DatetimeField(auto_now_add=True, index=True)
timestamp = fields.DatetimeField(auto_now_add=True, db_index=True)
idempotency_key = fields.CharField(max_length=255, unique=True, null=True)
metadata = fields.JSONField(null=True)
@@ -50,8 +54,9 @@ class UsageRecord(models.Model):
table = "usage_records"
indexes = [("user_id", "record_type", "timestamp")]
class UsageAggregate(models.Model):
id = fields.IntField(pk=True)
id = fields.IntField(primary_key=True)
user = fields.ForeignKeyField("models.User", related_name="usage_aggregates")
date = fields.DateField()
storage_bytes_avg = fields.BigIntField(default=0)
@@ -64,8 +69,9 @@ class UsageAggregate(models.Model):
table = "usage_aggregates"
unique_together = (("user", "date"),)
class Invoice(models.Model):
id = fields.IntField(pk=True)
id = fields.IntField(primary_key=True)
user = fields.ForeignKeyField("models.User", related_name="invoices")
invoice_number = fields.CharField(max_length=50, unique=True)
stripe_invoice_id = fields.CharField(max_length=255, unique=True, null=True)
@@ -75,10 +81,10 @@ class Invoice(models.Model):
tax = fields.DecimalField(max_digits=10, decimal_places=4, default=0)
total = fields.DecimalField(max_digits=10, decimal_places=4)
currency = fields.CharField(max_length=3, default="USD")
status = fields.CharField(max_length=50, default="draft", index=True)
status = fields.CharField(max_length=50, default="draft", db_index=True)
due_date = fields.DateField(null=True)
paid_at = fields.DatetimeField(null=True)
created_at = fields.DatetimeField(auto_now_add=True, index=True)
created_at = fields.DatetimeField(auto_now_add=True, db_index=True)
updated_at = fields.DatetimeField(auto_now=True)
metadata = fields.JSONField(null=True)
@@ -86,8 +92,9 @@ class Invoice(models.Model):
table = "invoices"
indexes = [("user_id", "status", "created_at")]
class InvoiceLineItem(models.Model):
id = fields.IntField(pk=True)
id = fields.IntField(primary_key=True)
invoice = fields.ForeignKeyField("billing.Invoice", related_name="line_items")
description = fields.TextField()
quantity = fields.DecimalField(max_digits=15, decimal_places=6)
@@ -100,20 +107,24 @@ class InvoiceLineItem(models.Model):
class Meta:
table = "invoice_line_items"
class PricingConfig(models.Model):
id = fields.IntField(pk=True)
id = fields.IntField(primary_key=True)
config_key = fields.CharField(max_length=100, unique=True)
config_value = fields.DecimalField(max_digits=10, decimal_places=6)
description = fields.TextField(null=True)
unit = fields.CharField(max_length=50, null=True)
updated_by = fields.ForeignKeyField("models.User", related_name="pricing_updates", null=True)
updated_by = fields.ForeignKeyField(
"models.User", related_name="pricing_updates", null=True
)
updated_at = fields.DatetimeField(auto_now=True)
class Meta:
table = "pricing_config"
class PaymentMethod(models.Model):
id = fields.IntField(pk=True)
id = fields.IntField(primary_key=True)
user = fields.ForeignKeyField("models.User", related_name="payment_methods")
stripe_payment_method_id = fields.CharField(max_length=255)
type = fields.CharField(max_length=50)
@@ -128,9 +139,12 @@ class PaymentMethod(models.Model):
class Meta:
table = "payment_methods"
class BillingEvent(models.Model):
id = fields.BigIntField(pk=True)
user = fields.ForeignKeyField("models.User", related_name="billing_events", null=True)
id = fields.BigIntField(primary_key=True)
user = fields.ForeignKeyField(
"models.User", related_name="billing_events", null=True
)
event_type = fields.CharField(max_length=100)
stripe_event_id = fields.CharField(max_length=255, unique=True, null=True)
data = fields.JSONField(null=True)
+9 -4
View File
@@ -1,7 +1,6 @@
from apscheduler.schedulers.asyncio import AsyncIOScheduler
from apscheduler.triggers.cron import CronTrigger
from datetime import datetime, date, timedelta
import asyncio
from .usage_tracker import UsageTracker
from .invoice_generator import InvoiceGenerator
@@ -9,6 +8,7 @@ from ..models import User
scheduler = AsyncIOScheduler()
async def aggregate_daily_usage_for_all_users():
users = await User.filter(is_active=True).all()
yesterday = date.today() - timedelta(days=1)
@@ -19,6 +19,7 @@ async def aggregate_daily_usage_for_all_users():
except Exception as e:
print(f"Failed to aggregate usage for user {user.id}: {e}")
async def generate_monthly_invoices():
now = datetime.now()
last_month = now.month - 1 if now.month > 1 else 12
@@ -28,19 +29,22 @@ async def generate_monthly_invoices():
for user in users:
try:
invoice = await InvoiceGenerator.generate_monthly_invoice(user, year, last_month)
invoice = await InvoiceGenerator.generate_monthly_invoice(
user, year, last_month
)
if invoice:
await InvoiceGenerator.finalize_invoice(invoice)
except Exception as e:
print(f"Failed to generate invoice for user {user.id}: {e}")
def start_scheduler():
scheduler.add_job(
aggregate_daily_usage_for_all_users,
CronTrigger(hour=1, minute=0),
id="aggregate_daily_usage",
name="Aggregate daily usage for all users",
replace_existing=True
replace_existing=True,
)
scheduler.add_job(
@@ -48,10 +52,11 @@ def start_scheduler():
CronTrigger(day=1, hour=2, minute=0),
id="generate_monthly_invoices",
name="Generate monthly invoices",
replace_existing=True
replace_existing=True,
)
scheduler.start()
def stop_scheduler():
scheduler.shutdown()
+19 -33
View File
@@ -1,8 +1,8 @@
import stripe
from decimal import Decimal
from typing import Optional, Dict, Any
from typing import Dict
from ..settings import settings
class StripeClient:
@staticmethod
def _ensure_api_key():
@@ -11,13 +11,12 @@ class StripeClient:
stripe.api_key = settings.STRIPE_SECRET_KEY
else:
raise ValueError("Stripe API key not configured")
@staticmethod
async def create_customer(email: str, name: str, metadata: Dict = None) -> str:
StripeClient._ensure_api_key()
customer = stripe.Customer.create(
email=email,
name=name,
metadata=metadata or {}
email=email, name=name, metadata=metadata or {}
)
return customer.id
@@ -26,7 +25,7 @@ class StripeClient:
amount: int,
currency: str = "usd",
customer_id: str = None,
metadata: Dict = None
metadata: Dict = None,
) -> stripe.PaymentIntent:
StripeClient._ensure_api_key()
return stripe.PaymentIntent.create(
@@ -34,32 +33,29 @@ class StripeClient:
currency=currency,
customer=customer_id,
metadata=metadata or {},
automatic_payment_methods={"enabled": True}
automatic_payment_methods={"enabled": True},
)
@staticmethod
async def create_invoice(
customer_id: str,
description: str,
line_items: list,
metadata: Dict = None
customer_id: str, description: str, line_items: list, metadata: Dict = None
) -> stripe.Invoice:
StripeClient._ensure_api_key()
for item in line_items:
stripe.InvoiceItem.create(
customer=customer_id,
amount=int(item['amount'] * 100),
currency=item.get('currency', 'usd'),
description=item['description'],
metadata=item.get('metadata', {})
amount=int(item["amount"] * 100),
currency=item.get("currency", "usd"),
description=item["description"],
metadata=item.get("metadata", {}),
)
invoice = stripe.Invoice.create(
customer=customer_id,
description=description,
auto_advance=True,
collection_method='charge_automatically',
metadata=metadata or {}
collection_method="charge_automatically",
metadata=metadata or {},
)
return invoice
@@ -76,18 +72,15 @@ class StripeClient:
@staticmethod
async def attach_payment_method(
payment_method_id: str,
customer_id: str
payment_method_id: str, customer_id: str
) -> stripe.PaymentMethod:
StripeClient._ensure_api_key()
payment_method = stripe.PaymentMethod.attach(
payment_method_id,
customer=customer_id
payment_method_id, customer=customer_id
)
stripe.Customer.modify(
customer_id,
invoice_settings={'default_payment_method': payment_method_id}
customer_id, invoice_settings={"default_payment_method": payment_method_id}
)
return payment_method
@@ -95,22 +88,15 @@ class StripeClient:
@staticmethod
async def list_payment_methods(customer_id: str, type: str = "card"):
StripeClient._ensure_api_key()
return stripe.PaymentMethod.list(
customer=customer_id,
type=type
)
return stripe.PaymentMethod.list(customer=customer_id, type=type)
@staticmethod
async def create_subscription(
customer_id: str,
price_id: str,
metadata: Dict = None
customer_id: str, price_id: str, metadata: Dict = None
) -> stripe.Subscription:
StripeClient._ensure_api_key()
return stripe.Subscription.create(
customer=customer_id,
items=[{'price': price_id}],
metadata=metadata or {}
customer=customer_id, items=[{"price": price_id}], metadata=metadata or {}
)
@staticmethod
+17 -17
View File
@@ -1,11 +1,10 @@
import uuid
from datetime import datetime, date, timezone
from decimal import Decimal
from typing import Optional
from tortoise.transactions import in_transaction
from .models import UsageRecord, UsageAggregate
from ..models import User
class UsageTracker:
@staticmethod
async def track_storage(
@@ -13,7 +12,7 @@ class UsageTracker:
amount_bytes: int,
resource_type: str = None,
resource_id: int = None,
metadata: dict = None
metadata: dict = None,
):
idempotency_key = f"storage_{user.id}_{datetime.now(timezone.utc).timestamp()}_{uuid.uuid4().hex[:8]}"
@@ -24,7 +23,7 @@ class UsageTracker:
resource_type=resource_type,
resource_id=resource_id,
idempotency_key=idempotency_key,
metadata=metadata
metadata=metadata,
)
@staticmethod
@@ -34,7 +33,7 @@ class UsageTracker:
direction: str = "down",
resource_type: str = None,
resource_id: int = None,
metadata: dict = None
metadata: dict = None,
):
record_type = f"bandwidth_{direction}"
idempotency_key = f"{record_type}_{user.id}_{datetime.now(timezone.utc).timestamp()}_{uuid.uuid4().hex[:8]}"
@@ -46,7 +45,7 @@ class UsageTracker:
resource_type=resource_type,
resource_id=resource_id,
idempotency_key=idempotency_key,
metadata=metadata
metadata=metadata,
)
@staticmethod
@@ -61,24 +60,26 @@ class UsageTracker:
user=user,
record_type="storage",
timestamp__gte=start_of_day,
timestamp__lte=end_of_day
timestamp__lte=end_of_day,
).all()
storage_avg = sum(r.amount_bytes for r in storage_records) // max(len(storage_records), 1)
storage_avg = sum(r.amount_bytes for r in storage_records) // max(
len(storage_records), 1
)
storage_peak = max((r.amount_bytes for r in storage_records), default=0)
bandwidth_up = await UsageRecord.filter(
user=user,
record_type="bandwidth_up",
timestamp__gte=start_of_day,
timestamp__lte=end_of_day
timestamp__lte=end_of_day,
).all()
bandwidth_down = await UsageRecord.filter(
user=user,
record_type="bandwidth_down",
timestamp__gte=start_of_day,
timestamp__lte=end_of_day
timestamp__lte=end_of_day,
).all()
total_up = sum(r.amount_bytes for r in bandwidth_up)
@@ -92,8 +93,8 @@ class UsageTracker:
"storage_bytes_avg": storage_avg,
"storage_bytes_peak": storage_peak,
"bandwidth_up_bytes": total_up,
"bandwidth_down_bytes": total_down
}
"bandwidth_down_bytes": total_down,
},
)
if not created:
@@ -108,6 +109,7 @@ class UsageTracker:
@staticmethod
async def get_current_storage(user: User) -> int:
from ..models import File
files = await File.filter(owner=user, is_deleted=False).all()
return sum(f.size for f in files)
@@ -121,9 +123,7 @@ class UsageTracker:
end_date = date(year, month, last_day)
aggregates = await UsageAggregate.filter(
user=user,
date__gte=start_date,
date__lte=end_date
user=user, date__gte=start_date, date__lte=end_date
).all()
if not aggregates:
@@ -132,7 +132,7 @@ class UsageTracker:
"storage_gb_peak": 0,
"bandwidth_up_gb": 0,
"bandwidth_down_gb": 0,
"total_bandwidth_gb": 0
"total_bandwidth_gb": 0,
}
storage_avg = sum(a.storage_bytes_avg for a in aggregates) / len(aggregates)
@@ -145,5 +145,5 @@ class UsageTracker:
"storage_gb_peak": round(storage_peak / (1024**3), 4),
"bandwidth_up_gb": round(bandwidth_up / (1024**3), 4),
"bandwidth_down_gb": round(bandwidth_down / (1024**3), 4),
"total_bandwidth_gb": round((bandwidth_up + bandwidth_down) / (1024**3), 4)
"total_bandwidth_gb": round((bandwidth_up + bandwidth_down) / (1024**3), 4),
}