chore: rename all project references from rbox to mywebdav across configs, code, and docs
This commit is contained in:
@@ -0,0 +1,187 @@
|
||||
from datetime import datetime, date, timedelta
|
||||
from decimal import Decimal
|
||||
from typing import Optional
|
||||
from calendar import monthrange
|
||||
from .models import Invoice, InvoiceLineItem, PricingConfig, UsageAggregate, 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]:
|
||||
period_start = date(year, month, 1)
|
||||
days_in_month = monthrange(year, month)[1]
|
||||
period_end = date(year, month, days_in_month)
|
||||
|
||||
usage = await UsageTracker.get_monthly_usage(user, year, month)
|
||||
|
||||
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_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)
|
||||
|
||||
import math
|
||||
billable_storage_rounded = Decimal(math.ceil(float(billable_storage)))
|
||||
billable_bandwidth_rounded = Decimal(math.ceil(float(billable_bandwidth)))
|
||||
|
||||
storage_cost = billable_storage_rounded * storage_price_per_gb
|
||||
bandwidth_cost = billable_bandwidth_rounded * bandwidth_price_per_gb
|
||||
|
||||
subtotal = storage_cost + bandwidth_cost
|
||||
|
||||
if subtotal <= 0:
|
||||
return None
|
||||
|
||||
tax_amount = subtotal * tax_rate
|
||||
total = subtotal + tax_amount
|
||||
|
||||
invoice_number = f"INV-{user.id:06d}-{year}{month:02d}"
|
||||
|
||||
subscription = await UserSubscription.get_or_none(user=user)
|
||||
|
||||
invoice = await Invoice.create(
|
||||
user=user,
|
||||
invoice_number=invoice_number,
|
||||
period_start=period_start,
|
||||
period_end=period_end,
|
||||
subtotal=subtotal,
|
||||
tax=tax_amount,
|
||||
total=total,
|
||||
currency="USD",
|
||||
status="draft",
|
||||
due_date=period_end + timedelta(days=7),
|
||||
metadata={
|
||||
"usage": usage,
|
||||
"pricing": {
|
||||
"storage_per_gb": float(storage_price_per_gb),
|
||||
"bandwidth_per_gb": float(bandwidth_price_per_gb)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
if billable_storage_rounded > 0:
|
||||
await InvoiceLineItem.create(
|
||||
invoice=invoice,
|
||||
description=f"Storage usage for {period_start.strftime('%B %Y')} (Average: {storage_gb:.2f} GB, Billable: {billable_storage_rounded} GB)",
|
||||
quantity=billable_storage_rounded,
|
||||
unit_price=storage_price_per_gb,
|
||||
amount=storage_cost,
|
||||
item_type="storage",
|
||||
metadata={"avg_gb": float(storage_gb), "free_gb": float(free_storage_gb)}
|
||||
)
|
||||
|
||||
if billable_bandwidth_rounded > 0:
|
||||
await InvoiceLineItem.create(
|
||||
invoice=invoice,
|
||||
description=f"Bandwidth usage for {period_start.strftime('%B %Y')} (Total: {bandwidth_gb:.2f} GB, Billable: {billable_bandwidth_rounded} GB)",
|
||||
quantity=billable_bandwidth_rounded,
|
||||
unit_price=bandwidth_price_per_gb,
|
||||
amount=bandwidth_cost,
|
||||
item_type="bandwidth",
|
||||
metadata={"total_gb": float(bandwidth_gb), "free_gb": float(free_bandwidth_gb)}
|
||||
)
|
||||
|
||||
if subscription and subscription.stripe_customer_id:
|
||||
try:
|
||||
line_items = await invoice.line_items.all()
|
||||
stripe_line_items = [
|
||||
{
|
||||
"amount": item.amount,
|
||||
"currency": "usd",
|
||||
"description": item.description,
|
||||
"metadata": item.metadata or {}
|
||||
}
|
||||
for item in line_items
|
||||
]
|
||||
|
||||
stripe_invoice = await StripeClient.create_invoice(
|
||||
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)}
|
||||
)
|
||||
|
||||
invoice.stripe_invoice_id = stripe_invoice.id
|
||||
await invoice.save()
|
||||
except Exception as e:
|
||||
print(f"Failed to create Stripe invoice: {e}")
|
||||
|
||||
return invoice
|
||||
|
||||
@staticmethod
|
||||
async def finalize_invoice(invoice: Invoice) -> Invoice:
|
||||
if invoice.status != "draft":
|
||||
raise ValueError("Only draft invoices can be finalized")
|
||||
|
||||
invoice.status = "open"
|
||||
await invoice.save()
|
||||
|
||||
if invoice.stripe_invoice_id:
|
||||
try:
|
||||
await StripeClient.finalize_invoice(invoice.stripe_invoice_id)
|
||||
except Exception as e:
|
||||
print(f"Failed to finalize Stripe invoice: {e}")
|
||||
|
||||
# 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])
|
||||
body = f"""Dear {invoice.user.username},
|
||||
|
||||
Your invoice {invoice.invoice_number} for the period {invoice.period_start} to {invoice.period_end} is now available.
|
||||
|
||||
Invoice Details:
|
||||
{items_text}
|
||||
|
||||
Subtotal: ${invoice.subtotal}
|
||||
Tax: ${invoice.tax}
|
||||
Total: ${invoice.total}
|
||||
|
||||
Due Date: {invoice.due_date}
|
||||
|
||||
You can view and pay your invoice at: {invoice.user.email} # Placeholder, should be a link to invoice page
|
||||
|
||||
Best regards,
|
||||
The MyWebdav Team
|
||||
"""
|
||||
html = f"""
|
||||
<h2>Invoice {invoice.invoice_number}</h2>
|
||||
<p>Dear {invoice.user.username},</p>
|
||||
<p>Your invoice for the period {invoice.period_start} to {invoice.period_end} is now available.</p>
|
||||
<table border="1">
|
||||
<tr><th>Description</th><th>Amount</th></tr>
|
||||
{"".join([f"<tr><td>{item.description}</td><td>${item.amount}</td></tr>" for item in line_items])}
|
||||
<tr><td><strong>Subtotal</strong></td><td><strong>${invoice.subtotal}</strong></td></tr>
|
||||
<tr><td><strong>Tax</strong></td><td><strong>${invoice.tax}</strong></td></tr>
|
||||
<tr><td><strong>Total</strong></td><td><strong>${invoice.total}</strong></td></tr>
|
||||
</table>
|
||||
<p>Due Date: {invoice.due_date}</p>
|
||||
<p>You can view and pay your invoice at: <a href="#">Invoice Link</a></p>
|
||||
<p>Best regards,<br>The MyWebdav Team</p>
|
||||
"""
|
||||
queue_email(
|
||||
to_email=invoice.user.email,
|
||||
subject=f"Your RBox Invoice {invoice.invoice_number}",
|
||||
body=body,
|
||||
html=html
|
||||
)
|
||||
|
||||
return invoice
|
||||
|
||||
@staticmethod
|
||||
async def mark_invoice_paid(invoice: Invoice) -> Invoice:
|
||||
invoice.status = "paid"
|
||||
invoice.paid_at = datetime.utcnow()
|
||||
await invoice.save()
|
||||
return invoice
|
||||
@@ -0,0 +1,141 @@
|
||||
from tortoise import fields, models
|
||||
from decimal import Decimal
|
||||
|
||||
class SubscriptionPlan(models.Model):
|
||||
id = fields.IntField(pk=True)
|
||||
name = fields.CharField(max_length=100, unique=True)
|
||||
display_name = fields.CharField(max_length=255)
|
||||
description = fields.TextField(null=True)
|
||||
storage_gb = fields.IntField()
|
||||
bandwidth_gb = fields.IntField()
|
||||
price_monthly = fields.DecimalField(max_digits=10, decimal_places=2)
|
||||
price_yearly = fields.DecimalField(max_digits=10, decimal_places=2, null=True)
|
||||
stripe_price_id = fields.CharField(max_length=255, null=True)
|
||||
is_active = fields.BooleanField(default=True)
|
||||
created_at = fields.DatetimeField(auto_now_add=True)
|
||||
updated_at = fields.DatetimeField(auto_now=True)
|
||||
|
||||
class Meta:
|
||||
table = "subscription_plans"
|
||||
|
||||
class UserSubscription(models.Model):
|
||||
id = fields.IntField(pk=True)
|
||||
user = fields.ForeignKeyField("models.User", related_name="subscription")
|
||||
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)
|
||||
status = fields.CharField(max_length=50, default="active")
|
||||
current_period_start = fields.DatetimeField(null=True)
|
||||
current_period_end = fields.DatetimeField(null=True)
|
||||
created_at = fields.DatetimeField(auto_now_add=True)
|
||||
updated_at = fields.DatetimeField(auto_now=True)
|
||||
canceled_at = fields.DatetimeField(null=True)
|
||||
|
||||
class Meta:
|
||||
table = "user_subscriptions"
|
||||
|
||||
class UsageRecord(models.Model):
|
||||
id = fields.BigIntField(pk=True)
|
||||
user = fields.ForeignKeyField("models.User", related_name="usage_records")
|
||||
record_type = fields.CharField(max_length=50, 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)
|
||||
idempotency_key = fields.CharField(max_length=255, unique=True, null=True)
|
||||
metadata = fields.JSONField(null=True)
|
||||
|
||||
class Meta:
|
||||
table = "usage_records"
|
||||
indexes = [("user_id", "record_type", "timestamp")]
|
||||
|
||||
class UsageAggregate(models.Model):
|
||||
id = fields.IntField(pk=True)
|
||||
user = fields.ForeignKeyField("models.User", related_name="usage_aggregates")
|
||||
date = fields.DateField()
|
||||
storage_bytes_avg = fields.BigIntField(default=0)
|
||||
storage_bytes_peak = fields.BigIntField(default=0)
|
||||
bandwidth_up_bytes = fields.BigIntField(default=0)
|
||||
bandwidth_down_bytes = fields.BigIntField(default=0)
|
||||
created_at = fields.DatetimeField(auto_now_add=True)
|
||||
|
||||
class Meta:
|
||||
table = "usage_aggregates"
|
||||
unique_together = (("user", "date"),)
|
||||
|
||||
class Invoice(models.Model):
|
||||
id = fields.IntField(pk=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)
|
||||
period_start = fields.DateField(index=True)
|
||||
period_end = fields.DateField()
|
||||
subtotal = fields.DecimalField(max_digits=10, decimal_places=4)
|
||||
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)
|
||||
due_date = fields.DateField(null=True)
|
||||
paid_at = fields.DatetimeField(null=True)
|
||||
created_at = fields.DatetimeField(auto_now_add=True, index=True)
|
||||
updated_at = fields.DatetimeField(auto_now=True)
|
||||
metadata = fields.JSONField(null=True)
|
||||
|
||||
class Meta:
|
||||
table = "invoices"
|
||||
indexes = [("user_id", "status", "created_at")]
|
||||
|
||||
class InvoiceLineItem(models.Model):
|
||||
id = fields.IntField(pk=True)
|
||||
invoice = fields.ForeignKeyField("billing.Invoice", related_name="line_items")
|
||||
description = fields.TextField()
|
||||
quantity = fields.DecimalField(max_digits=15, decimal_places=6)
|
||||
unit_price = fields.DecimalField(max_digits=10, decimal_places=6)
|
||||
amount = fields.DecimalField(max_digits=10, decimal_places=4)
|
||||
item_type = fields.CharField(max_length=50, null=True)
|
||||
metadata = fields.JSONField(null=True)
|
||||
created_at = fields.DatetimeField(auto_now_add=True)
|
||||
|
||||
class Meta:
|
||||
table = "invoice_line_items"
|
||||
|
||||
class PricingConfig(models.Model):
|
||||
id = fields.IntField(pk=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_at = fields.DatetimeField(auto_now=True)
|
||||
|
||||
class Meta:
|
||||
table = "pricing_config"
|
||||
|
||||
class PaymentMethod(models.Model):
|
||||
id = fields.IntField(pk=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)
|
||||
is_default = fields.BooleanField(default=False)
|
||||
last4 = fields.CharField(max_length=4, null=True)
|
||||
brand = fields.CharField(max_length=50, null=True)
|
||||
exp_month = fields.IntField(null=True)
|
||||
exp_year = fields.IntField(null=True)
|
||||
created_at = fields.DatetimeField(auto_now_add=True)
|
||||
updated_at = fields.DatetimeField(auto_now=True)
|
||||
|
||||
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)
|
||||
event_type = fields.CharField(max_length=100)
|
||||
stripe_event_id = fields.CharField(max_length=255, unique=True, null=True)
|
||||
data = fields.JSONField(null=True)
|
||||
processed = fields.BooleanField(default=False)
|
||||
created_at = fields.DatetimeField(auto_now_add=True)
|
||||
|
||||
class Meta:
|
||||
table = "billing_events"
|
||||
@@ -0,0 +1,57 @@
|
||||
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
|
||||
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)
|
||||
|
||||
for user in users:
|
||||
try:
|
||||
await UsageTracker.aggregate_daily_usage(user, yesterday)
|
||||
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
|
||||
year = now.year if now.month > 1 else now.year - 1
|
||||
|
||||
users = await User.filter(is_active=True).all()
|
||||
|
||||
for user in users:
|
||||
try:
|
||||
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
|
||||
)
|
||||
|
||||
scheduler.add_job(
|
||||
generate_monthly_invoices,
|
||||
CronTrigger(day=1, hour=2, minute=0),
|
||||
id="generate_monthly_invoices",
|
||||
name="Generate monthly invoices",
|
||||
replace_existing=True
|
||||
)
|
||||
|
||||
scheduler.start()
|
||||
|
||||
def stop_scheduler():
|
||||
scheduler.shutdown()
|
||||
@@ -0,0 +1,119 @@
|
||||
import stripe
|
||||
from decimal import Decimal
|
||||
from typing import Optional, Dict, Any
|
||||
from ..settings import settings
|
||||
|
||||
class StripeClient:
|
||||
@staticmethod
|
||||
def _ensure_api_key():
|
||||
if not stripe.api_key:
|
||||
if settings.STRIPE_SECRET_KEY:
|
||||
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 {}
|
||||
)
|
||||
return customer.id
|
||||
|
||||
@staticmethod
|
||||
async def create_payment_intent(
|
||||
amount: int,
|
||||
currency: str = "usd",
|
||||
customer_id: str = None,
|
||||
metadata: Dict = None
|
||||
) -> stripe.PaymentIntent:
|
||||
StripeClient._ensure_api_key()
|
||||
return stripe.PaymentIntent.create(
|
||||
amount=amount,
|
||||
currency=currency,
|
||||
customer=customer_id,
|
||||
metadata=metadata or {},
|
||||
automatic_payment_methods={"enabled": True}
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
async def create_invoice(
|
||||
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', {})
|
||||
)
|
||||
|
||||
invoice = stripe.Invoice.create(
|
||||
customer=customer_id,
|
||||
description=description,
|
||||
auto_advance=True,
|
||||
collection_method='charge_automatically',
|
||||
metadata=metadata or {}
|
||||
)
|
||||
|
||||
return invoice
|
||||
|
||||
@staticmethod
|
||||
async def finalize_invoice(invoice_id: str) -> stripe.Invoice:
|
||||
StripeClient._ensure_api_key()
|
||||
return stripe.Invoice.finalize_invoice(invoice_id)
|
||||
|
||||
@staticmethod
|
||||
async def pay_invoice(invoice_id: str) -> stripe.Invoice:
|
||||
StripeClient._ensure_api_key()
|
||||
return stripe.Invoice.pay(invoice_id)
|
||||
|
||||
@staticmethod
|
||||
async def attach_payment_method(
|
||||
payment_method_id: str,
|
||||
customer_id: str
|
||||
) -> stripe.PaymentMethod:
|
||||
StripeClient._ensure_api_key()
|
||||
payment_method = stripe.PaymentMethod.attach(
|
||||
payment_method_id,
|
||||
customer=customer_id
|
||||
)
|
||||
|
||||
stripe.Customer.modify(
|
||||
customer_id,
|
||||
invoice_settings={'default_payment_method': payment_method_id}
|
||||
)
|
||||
|
||||
return payment_method
|
||||
|
||||
@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
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
async def create_subscription(
|
||||
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 {}
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
async def cancel_subscription(subscription_id: str) -> stripe.Subscription:
|
||||
StripeClient._ensure_api_key()
|
||||
return stripe.Subscription.delete(subscription_id)
|
||||
@@ -0,0 +1,149 @@
|
||||
import uuid
|
||||
from datetime import datetime, date
|
||||
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(
|
||||
user: User,
|
||||
amount_bytes: int,
|
||||
resource_type: str = None,
|
||||
resource_id: int = None,
|
||||
metadata: dict = None
|
||||
):
|
||||
idempotency_key = f"storage_{user.id}_{datetime.utcnow().timestamp()}_{uuid.uuid4().hex[:8]}"
|
||||
|
||||
await UsageRecord.create(
|
||||
user=user,
|
||||
record_type="storage",
|
||||
amount_bytes=amount_bytes,
|
||||
resource_type=resource_type,
|
||||
resource_id=resource_id,
|
||||
idempotency_key=idempotency_key,
|
||||
metadata=metadata
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
async def track_bandwidth(
|
||||
user: User,
|
||||
amount_bytes: int,
|
||||
direction: str = "down",
|
||||
resource_type: str = None,
|
||||
resource_id: int = None,
|
||||
metadata: dict = None
|
||||
):
|
||||
record_type = f"bandwidth_{direction}"
|
||||
idempotency_key = f"{record_type}_{user.id}_{datetime.utcnow().timestamp()}_{uuid.uuid4().hex[:8]}"
|
||||
|
||||
await UsageRecord.create(
|
||||
user=user,
|
||||
record_type=record_type,
|
||||
amount_bytes=amount_bytes,
|
||||
resource_type=resource_type,
|
||||
resource_id=resource_id,
|
||||
idempotency_key=idempotency_key,
|
||||
metadata=metadata
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
async def aggregate_daily_usage(user: User, target_date: date = None):
|
||||
if target_date is None:
|
||||
target_date = date.today()
|
||||
|
||||
start_of_day = datetime.combine(target_date, datetime.min.time())
|
||||
end_of_day = datetime.combine(target_date, datetime.max.time())
|
||||
|
||||
storage_records = await UsageRecord.filter(
|
||||
user=user,
|
||||
record_type="storage",
|
||||
timestamp__gte=start_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_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
|
||||
).all()
|
||||
|
||||
bandwidth_down = await UsageRecord.filter(
|
||||
user=user,
|
||||
record_type="bandwidth_down",
|
||||
timestamp__gte=start_of_day,
|
||||
timestamp__lte=end_of_day
|
||||
).all()
|
||||
|
||||
total_up = sum(r.amount_bytes for r in bandwidth_up)
|
||||
total_down = sum(r.amount_bytes for r in bandwidth_down)
|
||||
|
||||
async with in_transaction():
|
||||
aggregate, created = await UsageAggregate.get_or_create(
|
||||
user=user,
|
||||
date=target_date,
|
||||
defaults={
|
||||
"storage_bytes_avg": storage_avg,
|
||||
"storage_bytes_peak": storage_peak,
|
||||
"bandwidth_up_bytes": total_up,
|
||||
"bandwidth_down_bytes": total_down
|
||||
}
|
||||
)
|
||||
|
||||
if not created:
|
||||
aggregate.storage_bytes_avg = storage_avg
|
||||
aggregate.storage_bytes_peak = storage_peak
|
||||
aggregate.bandwidth_up_bytes = total_up
|
||||
aggregate.bandwidth_down_bytes = total_down
|
||||
await aggregate.save()
|
||||
|
||||
return aggregate
|
||||
|
||||
@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)
|
||||
|
||||
@staticmethod
|
||||
async def get_monthly_usage(user: User, year: int, month: int) -> dict:
|
||||
from datetime import date
|
||||
from calendar import monthrange
|
||||
|
||||
start_date = date(year, month, 1)
|
||||
_, last_day = monthrange(year, month)
|
||||
end_date = date(year, month, last_day)
|
||||
|
||||
aggregates = await UsageAggregate.filter(
|
||||
user=user,
|
||||
date__gte=start_date,
|
||||
date__lte=end_date
|
||||
).all()
|
||||
|
||||
if not aggregates:
|
||||
return {
|
||||
"storage_gb_avg": 0,
|
||||
"storage_gb_peak": 0,
|
||||
"bandwidth_up_gb": 0,
|
||||
"bandwidth_down_gb": 0,
|
||||
"total_bandwidth_gb": 0
|
||||
}
|
||||
|
||||
storage_avg = sum(a.storage_bytes_avg for a in aggregates) / len(aggregates)
|
||||
storage_peak = max(a.storage_bytes_peak for a in aggregates)
|
||||
bandwidth_up = sum(a.bandwidth_up_bytes for a in aggregates)
|
||||
bandwidth_down = sum(a.bandwidth_down_bytes for a in aggregates)
|
||||
|
||||
return {
|
||||
"storage_gb_avg": round(storage_avg / (1024**3), 4),
|
||||
"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)
|
||||
}
|
||||
Reference in New Issue
Block a user