feat: implement tiered subscription plans with usage-based pricing and dynamic plan management

Add multi-tier subscription system (Starter, Professional, Enterprise) with per-tier storage and bandwidth pricing, replace static free-tier limits with nullable usage-based fields in SubscriptionPlan model, introduce subscribe/unsubscribe API endpoints with plan validation, update invoice generator to calculate costs based on user's active plan tier, and refactor pricing page to load plans dynamically from backend API instead of hardcoded HTML.
This commit is contained in:
retoor 2026-01-22 15:40:16 +00:00
parent a34668941f
commit 619e320780
10 changed files with 445 additions and 148 deletions

View File

@ -99,11 +99,13 @@ init-db:
await Tortoise.generate_schemas(); \
count = await PricingConfig.all().count(); \
if count == 0: \
await PricingConfig.create(config_key='storage_per_gb_month', config_value=Decimal('0.0045'), description='Storage cost per GB per month', unit='per_gb_month'); \
await PricingConfig.create(config_key='bandwidth_egress_per_gb', config_value=Decimal('0.009'), description='Bandwidth egress cost per GB', unit='per_gb'); \
await PricingConfig.create(config_key='storage_per_gb_month', config_value=Decimal('0.005'), description='Storage cost per GB per month (Starter tier)', unit='per_gb_month'); \
await PricingConfig.create(config_key='storage_per_gb_month_pro', config_value=Decimal('0.004'), description='Storage cost per GB per month (Professional tier)', unit='per_gb_month'); \
await PricingConfig.create(config_key='storage_per_gb_month_enterprise', config_value=Decimal('0.003'), description='Storage cost per GB per month (Enterprise tier, 10TB+)', unit='per_gb_month'); \
await PricingConfig.create(config_key='bandwidth_egress_per_gb', config_value=Decimal('0.008'), description='Bandwidth egress cost per GB (Starter tier)', unit='per_gb'); \
await PricingConfig.create(config_key='bandwidth_egress_per_gb_pro', config_value=Decimal('0.007'), description='Bandwidth egress cost per GB (Professional tier)', unit='per_gb'); \
await PricingConfig.create(config_key='bandwidth_egress_per_gb_enterprise', config_value=Decimal('0.005'), description='Bandwidth egress cost per GB (Enterprise tier)', unit='per_gb'); \
await PricingConfig.create(config_key='bandwidth_ingress_per_gb', config_value=Decimal('0.0'), description='Bandwidth ingress cost per GB (free)', unit='per_gb'); \
await PricingConfig.create(config_key='free_tier_storage_gb', config_value=Decimal('15'), description='Free tier storage in GB', unit='gb'); \
await PricingConfig.create(config_key='free_tier_bandwidth_gb', config_value=Decimal('15'), description='Free tier bandwidth in GB per month', unit='gb'); \
await PricingConfig.create(config_key='tax_rate_default', config_value=Decimal('0.0'), description='Default tax rate (0 = no tax)', unit='percentage'); \
print('Default pricing configuration created'); \
else: \

View File

@ -22,14 +22,49 @@ 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"))
# Get user's subscription plan
user_subscription = await UserSubscription.get_or_none(user=user)
plan_name = "starter" # Default to starter
if user_subscription and user_subscription.plan:
plan_name = user_subscription.plan.name
# Set pricing based on subscription tier
if plan_name == "professional":
storage_price_per_gb = pricing_dict.get(
"storage_per_gb_month_professional", Decimal("0.004")
)
bandwidth_price_per_gb = pricing_dict.get(
"bandwidth_egress_per_gb_professional", Decimal("0.007")
)
elif plan_name == "enterprise":
storage_price_per_gb = pricing_dict.get(
"storage_per_gb_month_enterprise", Decimal("0.003")
)
bandwidth_price_per_gb = pricing_dict.get(
"bandwidth_egress_per_gb_enterprise", Decimal("0.005")
)
# Check if user meets minimum storage requirement for enterprise pricing
enterprise_min_tb = pricing_dict.get("enterprise_min_storage_tb", Decimal("10"))
storage_gb = Decimal(str(usage["storage_gb_avg"]))
if storage_gb < (enterprise_min_tb * Decimal("1024")): # Convert TB to GB
# User doesn't meet enterprise minimum, fall back to professional
storage_price_per_gb = pricing_dict.get(
"storage_per_gb_month_professional", Decimal("0.004")
)
bandwidth_price_per_gb = pricing_dict.get(
"bandwidth_egress_per_gb_professional", Decimal("0.007")
)
else: # starter
storage_price_per_gb = pricing_dict.get(
"storage_per_gb_month_starter", Decimal("0.005")
)
bandwidth_price_per_gb = pricing_dict.get(
"bandwidth_egress_per_gb_starter", Decimal("0.008")
)
# No free tier - charge from first GB
free_storage_gb = Decimal("0")
free_bandwidth_gb = Decimal("0")
tax_rate = pricing_dict.get("tax_rate_default", Decimal("0"))
storage_gb = Decimal(str(usage["storage_gb_avg"]))

View File

@ -6,8 +6,8 @@ class SubscriptionPlan(models.Model):
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()
storage_gb = fields.IntField(null=True) # null means unlimited/usage-based
bandwidth_gb = fields.IntField(null=True) # null means unlimited/usage-based
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)

View File

@ -146,27 +146,81 @@ async def lifespan(app: FastAPI):
logger.info("Database connected.")
from .billing.scheduler import start_scheduler
from .billing.models import PricingConfig
from .billing.models import PricingConfig, SubscriptionPlan
from .mail import email_service
start_scheduler()
logger.info("Billing scheduler started")
await email_service.start()
logger.info("Email service started")
pricing_count = await PricingConfig.all().count()
if pricing_count == 0:
plan_count = await SubscriptionPlan.all().count()
if plan_count == 0:
from decimal import Decimal
# Create subscription plans
await SubscriptionPlan.create(
name="starter",
display_name="Starter",
description="Perfect for individuals and small projects",
storage_gb=None,
bandwidth_gb=None,
price_monthly=Decimal("0.00"),
is_active=True
)
await SubscriptionPlan.create(
name="professional",
display_name="Professional",
description="Best for growing teams and businesses",
storage_gb=None,
bandwidth_gb=None,
price_monthly=Decimal("0.00"),
is_active=True
)
await SubscriptionPlan.create(
name="enterprise",
display_name="Enterprise",
description="For large organizations with high volume needs",
storage_gb=None,
bandwidth_gb=None,
price_monthly=Decimal("0.00"),
is_active=True
)
# Create tiered pricing configuration
await PricingConfig.create(
config_key="storage_per_gb_month",
config_value=Decimal("0.0045"),
description="Storage cost per GB per month",
config_key="storage_per_gb_month_starter",
config_value=Decimal("0.005"),
description="Storage cost per GB per month (Starter tier)",
unit="per_gb_month",
)
await PricingConfig.create(
config_key="bandwidth_egress_per_gb",
config_value=Decimal("0.009"),
description="Bandwidth egress cost per GB",
config_key="storage_per_gb_month_professional",
config_value=Decimal("0.004"),
description="Storage cost per GB per month (Professional tier)",
unit="per_gb_month",
)
await PricingConfig.create(
config_key="storage_per_gb_month_enterprise",
config_value=Decimal("0.003"),
description="Storage cost per GB per month (Enterprise tier, 10TB+)",
unit="per_gb_month",
)
await PricingConfig.create(
config_key="bandwidth_egress_per_gb_starter",
config_value=Decimal("0.008"),
description="Bandwidth egress cost per GB (Starter tier)",
unit="per_gb",
)
await PricingConfig.create(
config_key="bandwidth_egress_per_gb_professional",
config_value=Decimal("0.007"),
description="Bandwidth egress cost per GB (Professional tier)",
unit="per_gb",
)
await PricingConfig.create(
config_key="bandwidth_egress_per_gb_enterprise",
config_value=Decimal("0.005"),
description="Bandwidth egress cost per GB (Enterprise tier)",
unit="per_gb",
)
await PricingConfig.create(
@ -175,25 +229,19 @@ async def lifespan(app: FastAPI):
description="Bandwidth ingress cost per GB (free)",
unit="per_gb",
)
await PricingConfig.create(
config_key="free_tier_storage_gb",
config_value=Decimal("15"),
description="Free tier storage in GB",
unit="gb",
)
await PricingConfig.create(
config_key="free_tier_bandwidth_gb",
config_value=Decimal("15"),
description="Free tier bandwidth in GB per month",
unit="gb",
)
await PricingConfig.create(
config_key="tax_rate_default",
config_value=Decimal("0.0"),
description="Default tax rate (0 = no tax)",
unit="percentage",
)
logger.info("Default pricing configuration initialized")
await PricingConfig.create(
config_key="enterprise_min_storage_tb",
config_value=Decimal("10"),
description="Minimum storage for enterprise pricing (TB)",
unit="tb",
)
logger.info("Subscription plans and tiered pricing configuration initialized")
yield

View File

@ -234,3 +234,17 @@ async def get_new_recovery_codes(
await current_user.save()
return recovery_codes
@router.get("/me")
async def get_current_user_info(current_user: User = Depends(get_current_user)):
"""Get current user information"""
return {
"id": current_user.id,
"username": current_user.username,
"email": current_user.email,
"is_active": current_user.is_active,
"is_verified": current_user.is_verified,
"is_2fa_enabled": current_user.is_2fa_enabled,
"created_at": current_user.created_at.isoformat() if current_user.created_at else None,
}

View File

@ -408,6 +408,145 @@ async def list_plans():
]
class SubscribeRequest(BaseModel):
plan_name: str
class UnsubscribeRequest(BaseModel):
cancel_immediately: bool = False
@router.post("/subscribe")
async def subscribe_to_plan(
request: SubscribeRequest,
current_user: User = Depends(get_current_user),
):
try:
# Find the plan
plan = await SubscriptionPlan.get_or_none(
name=request.plan_name,
is_active=True
)
if not plan:
raise HTTPException(
status_code=404,
detail=f"Plan '{request.plan_name}' not found"
)
# Check if user already has a subscription
existing_subscription = await UserSubscription.get_or_none(
user=current_user
)
if existing_subscription:
# Update existing subscription
existing_subscription.plan = plan
existing_subscription.billing_type = "subscription"
await existing_subscription.save()
return {
"message": f"Successfully updated to {plan.display_name} plan",
"plan": plan.display_name,
"billing_type": "subscription"
}
else:
# Create new subscription
subscription = await UserSubscription.create(
user=current_user,
plan=plan,
billing_type="subscription",
status="active"
)
return {
"message": f"Successfully subscribed to {plan.display_name} plan",
"plan": plan.display_name,
"billing_type": "subscription",
"subscription_id": subscription.id
}
except Exception as e:
raise HTTPException(
status_code=500,
detail=f"Failed to subscribe to plan: {str(e)}"
)
@router.post("/unsubscribe")
async def unsubscribe_from_plan(
request: UnsubscribeRequest,
current_user: User = Depends(get_current_user),
):
try:
subscription = await UserSubscription.get_or_none(
user=current_user
)
if not subscription:
raise HTTPException(
status_code=404,
detail="No active subscription found"
)
if request.cancel_immediately:
# Cancel subscription immediately
await subscription.delete()
return {"message": "Subscription cancelled immediately"}
else:
# Mark for cancellation at end of billing period
subscription.status = "cancelled"
await subscription.save()
return {"message": "Subscription will be cancelled at end of billing period"}
except Exception as e:
raise HTTPException(
status_code=500,
detail=f"Failed to unsubscribe: {str(e)}"
)
@router.get("/subscription")
async def get_subscription(
current_user: User = Depends(get_current_user),
) -> SubscriptionResponse:
try:
subscription = await UserSubscription.get_or_none(
user=current_user
).prefetch_related("plan")
if not subscription:
# Return default starter subscription
default_plan = await SubscriptionPlan.get_or_none(
name="starter",
is_active=True
)
return SubscriptionResponse(
id=0,
billing_type="pay_as_you_go",
plan_name=default_plan.display_name if default_plan else "Starter",
status="active",
current_period_start=None,
current_period_end=None
)
return SubscriptionResponse(
id=subscription.id,
billing_type=subscription.billing_type,
plan_name=subscription.plan.display_name if subscription.plan else None,
status=subscription.status,
current_period_start=subscription.current_period_start,
current_period_end=subscription.current_period_end
)
except Exception as e:
raise HTTPException(
status_code=500,
detail=f"Failed to fetch subscription: {str(e)}"
)
@router.get("/stripe-key")
async def get_stripe_key():
from ..settings import settings

View File

@ -238,65 +238,22 @@ endblock %}
<p class="page-subtitle">Pay only for what you use. No hidden fees, no surprises.</p>
<div class="pricing-hero">
<h2 style="font-size: 2rem; margin: 0;">Pay-As-You-Go Storage</h2>
<div class="pricing-hero-amount">$5/TB</div>
<p class="pricing-hero-description">Plus 15GB free tier included</p>
<h2 style="font-size: 2rem; margin: 0;">Simple Cloud Storage Pricing</h2>
<div class="pricing-hero-amount">$3-$5/TB</div>
<p class="pricing-hero-description">Pay only for what you use. No hidden features, just storage.</p>
</div>
<div class="pricing-grid">
<div class="pricing-grid" id="pricing-plans">
<!-- Plans will be loaded dynamically from API -->
<div class="pricing-card">
<div class="pricing-tier">Free Tier</div>
<div class="pricing-amount">$0</div>
<div class="pricing-period">First 15GB included</div>
<div class="pricing-tier">Loading...</div>
<div class="pricing-amount">$-</div>
<div class="pricing-period">Loading plans...</div>
<ul class="pricing-features">
<li>15GB storage included</li>
<li>15GB bandwidth per month</li>
<li>All core features</li>
<li>WebDAV support</li>
<li>File versioning</li>
<li>Two-factor authentication</li>
<li>EU data residency</li>
<li>Loading subscription plans...</li>
</ul>
<div class="pricing-cta">
<a href="/app" class="btn btn-secondary" style="width: 100%;">Get Started Free</a>
</div>
</div>
<div class="pricing-card featured">
<div class="pricing-tier">Pay-As-You-Go</div>
<div class="pricing-amount">$5/TB</div>
<div class="pricing-period">Billed monthly</div>
<ul class="pricing-features">
<li>$0.0045 per GB per month storage</li>
<li>$0.009 per GB egress bandwidth</li>
<li>Free ingress bandwidth</li>
<li>All premium features</li>
<li>99.9% uptime SLA</li>
<li>24/7 support</li>
<li>API access</li>
<li>Priority support</li>
</ul>
<div class="pricing-cta">
<a href="/app" class="btn btn-primary" style="width: 100%;">Start Using</a>
</div>
</div>
<div class="pricing-card">
<div class="pricing-tier">Enterprise</div>
<div class="pricing-amount">Custom</div>
<div class="pricing-period">Contact us for pricing</div>
<ul class="pricing-features">
<li>Volume discounts available</li>
<li>Dedicated account manager</li>
<li>Custom SLA options</li>
<li>Priority support 24/7</li>
<li>Custom integrations</li>
<li>Training and onboarding</li>
<li>Compliance assistance</li>
<li>Secure infrastructure</li>
</ul>
<div class="pricing-cta">
<a href="/support" class="btn btn-secondary" style="width: 100%;">Contact Sales</a>
<button class="btn btn-secondary" style="width: 100%;" disabled>Loading...</button>
</div>
</div>
</div>
@ -327,60 +284,191 @@ endblock %}
<div class="faq-item">
<div class="faq-question">How is storage calculated?</div>
<div class="faq-answer">Storage is calculated based on your average daily usage throughout the month. You're
charged $0.0045 per GB per month ($5 per TB). The first 15GB is always free.</div>
charged per GB per month starting from the first GB. No free tiers or hidden limits.</div>
</div>
<div class="faq-item">
<div class="faq-question">What is bandwidth egress?</div>
<div class="faq-answer">Bandwidth egress is data transferred out of MyWebdav (downloads). You're charged
$0.009 per GB. The first 15GB per month is free. Uploads (ingress) are always free.</div>
per GB for downloads. Uploads (ingress) are always free.</div>
</div>
<div class="faq-item">
<div class="faq-question">Are there any hidden fees?</div>
<div class="faq-answer">No. We believe in transparent pricing. You only pay for storage and egress bandwidth
as shown. There are no setup fees, minimum charges, or surprise costs.</div>
<div class="faq-answer">No. You only pay for storage usage and download bandwidth. No setup fees,
no minimum charges, no surprise costs. The price you see is what you pay.</div>
</div>
<div class="faq-item">
<div class="faq-question">Can I cancel anytime?</div>
<div class="faq-answer">Yes. There are no long-term contracts or commitments. You can delete your account at
any time and will only be charged for usage up to that point.</div>
<div class="faq-answer">Yes. There are no long-term contracts or commitments. You can close your account at
any time and will only be charged for actual usage up to that point.</div>
</div>
<div class="faq-item">
<div class="faq-question">What payment methods do you accept?</div>
<div class="faq-answer">We accept all major credit cards (Visa, Mastercard, American Express) and SEPA
direct debit through our payment processor Stripe.</div>
<div class="faq-answer">We accept all major credit cards through our payment processor Stripe.</div>
</div>
<div class="faq-item">
<div class="faq-question">Do you offer volume discounts?</div>
<div class="faq-answer">Yes. For storage over 10TB or bandwidth over 5TB per month, please contact our sales
team for custom pricing.</div>
<div class="faq-answer">Yes. Our Enterprise tier offers lower rates for high volume usage (10TB+).
Contact us for details on volume pricing.</div>
</div>
</div>
</div>
<script>
async function loadSubscriptionPlans() {
try {
const response = await fetch('/api/billing/plans');
if (!response.ok) throw new Error('Failed to load plans');
const plans = await response.json();
const plansContainer = document.getElementById('pricing-plans');
plansContainer.innerHTML = plans.map(plan => {
const isFeatured = plan.name === 'professional';
// Set pricing based on plan name
let storagePrice, bandwidthPrice, ctaText, ctaAction, features;
if (plan.name === 'starter') {
storagePrice = '$0.005/GB';
bandwidthPrice = '$0.008/GB';
ctaText = 'Get Started';
ctaAction = () => subscribeToPlan('starter');
features = [
'Cloud storage for individuals',
'Storage: ' + storagePrice,
'Bandwidth: ' + bandwidthPrice,
'Free uploads (ingress)',
'WebDAV access',
'SFTP access',
'API access',
'File versioning',
'99.9% uptime SLA',
'Email support'
];
} else if (plan.name === 'professional') {
storagePrice = '$0.004/GB';
bandwidthPrice = '$0.007/GB';
ctaText = 'Choose Plan';
ctaAction = () => subscribeToPlan('professional');
features = [
'Cloud storage for professionals',
'Storage: ' + storagePrice,
'Bandwidth: ' + bandwidthPrice,
'Free uploads (ingress)',
'WebDAV access',
'SFTP access',
'API access',
'File versioning',
'99.9% uptime SLA',
'Priority email support'
];
} else if (plan.name === 'enterprise') {
storagePrice = '$0.003/GB (10TB+)';
bandwidthPrice = '$0.005/GB';
ctaText = 'Contact Sales';
ctaAction = () => window.location.href = '/support';
features = [
'Cloud storage for enterprises',
'Storage: ' + storagePrice,
'Bandwidth: ' + bandwidthPrice,
'Free uploads (ingress)',
'WebDAV access',
'SFTP access',
'API access',
'File versioning',
'99.9% uptime SLA',
'Priority email support',
'Volume discounts available'
];
}
return `
<div class="pricing-card ${isFeatured ? 'featured' : ''}">
<div class="pricing-tier">${plan.display_name}</div>
<div class="pricing-amount">${plan.price_monthly > 0 ? '$' + plan.price_monthly : 'Usage-based'}</div>
<div class="pricing-period">Billed monthly based on usage</div>
<ul class="pricing-features">
${features.map(feature => `<li>${feature}</li>`).join('')}
</ul>
<div class="pricing-cta">
<button class="btn ${isFeatured ? 'btn-primary' : 'btn-secondary'}"
style="width: 100%;"
onclick="(${ctaAction})()">
${ctaText}
</button>
</div>
</div>
`;
}).join('');
} catch (error) {
console.error('Error loading plans:', error);
document.getElementById('pricing-plans').innerHTML = `
<div class="pricing-card">
<div class="pricing-tier">Error</div>
<div class="pricing-amount">-</div>
<div class="pricing-period">Failed to load plans</div>
<ul class="pricing-features">
<li>Please refresh the page and try again</li>
</ul>
</div>
`;
}
}
async function subscribeToPlan(planName) {
try {
// Check if user is logged in
const response = await fetch('/api/auth/me');
if (!response.ok) {
// User not logged in, redirect to login
window.location.href = '/app#login';
return;
}
const subscribeResponse = await fetch('/api/billing/subscribe', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ plan_name: planName })
});
if (!subscribeResponse.ok) {
const error = await subscribeResponse.json();
throw new Error(error.detail || 'Failed to subscribe');
}
const result = await subscribeResponse.json();
alert(result.message);
} catch (error) {
console.error('Subscription error:', error);
alert(error.message || 'Failed to subscribe. Please try again.');
}
}
function calculatePrice() {
const storage = parseFloat(document.getElementById('storage').value) || 0;
const bandwidth = parseFloat(document.getElementById('bandwidth').value) || 0;
const freeStorage = 15;
const freeBandwidth = 15;
const billableStorage = Math.max(0, storage - freeStorage);
const billableBandwidth = Math.max(0, bandwidth - freeBandwidth);
const storageCost = billableStorage * 0.0045;
const bandwidthCost = billableBandwidth * 0.009;
// Professional tier pricing (featured)
const storageCost = storage * 0.004;
const bandwidthCost = bandwidth * 0.007;
const total = storageCost + bandwidthCost;
document.getElementById('result').textContent = '$' + total.toFixed(2);
}
calculatePrice();
// Load plans when page loads
document.addEventListener('DOMContentLoaded', function() {
loadSubscriptionPlans();
calculatePrice();
});
</script>
{% endblock %}

View File

@ -59,7 +59,7 @@ async def pricing_config():
configs.append(
await PricingConfig.create(
config_key="storage_per_gb_month",
config_value=Decimal("0.0045"),
config_value=Decimal("0.005"),
description="Storage cost per GB per month",
unit="per_gb_month",
)
@ -67,27 +67,11 @@ async def pricing_config():
configs.append(
await PricingConfig.create(
config_key="bandwidth_egress_per_gb",
config_value=Decimal("0.009"),
config_value=Decimal("0.008"),
description="Bandwidth egress cost per GB",
unit="per_gb",
)
)
configs.append(
await PricingConfig.create(
config_key="free_tier_storage_gb",
config_value=Decimal("15"),
description="Free tier storage in GB",
unit="gb",
)
)
configs.append(
await PricingConfig.create(
config_key="free_tier_bandwidth_gb",
config_value=Decimal("15"),
description="Free tier bandwidth in GB per month",
unit="gb",
)
)
yield configs
for config in configs:
await config.delete()

View File

@ -31,7 +31,7 @@ async def pricing_config():
configs.append(
await PricingConfig.create(
config_key="storage_per_gb_month",
config_value=Decimal("0.0045"),
config_value=Decimal("0.005"),
description="Storage cost per GB per month",
unit="per_gb_month",
)
@ -39,27 +39,11 @@ async def pricing_config():
configs.append(
await PricingConfig.create(
config_key="bandwidth_egress_per_gb",
config_value=Decimal("0.009"),
config_value=Decimal("0.008"),
description="Bandwidth egress cost per GB",
unit="per_gb",
)
)
configs.append(
await PricingConfig.create(
config_key="free_tier_storage_gb",
config_value=Decimal("15"),
description="Free tier storage in GB",
unit="gb",
)
)
configs.append(
await PricingConfig.create(
config_key="free_tier_bandwidth_gb",
config_value=Decimal("15"),
description="Free tier bandwidth in GB per month",
unit="gb",
)
)
configs.append(
await PricingConfig.create(
config_key="tax_rate_default",
@ -103,7 +87,7 @@ async def test_generate_monthly_invoice_with_usage(test_user, pricing_config):
@pytest.mark.asyncio
async def test_generate_monthly_invoice_below_free_tier(test_user, pricing_config):
async def test_generate_monthly_invoice_with_small_usage(test_user, pricing_config):
today = date.today()
await UsageAggregate.create(
@ -119,8 +103,11 @@ async def test_generate_monthly_invoice_below_free_tier(test_user, pricing_confi
test_user, today.year, today.month
)
assert invoice is None
# Should always generate invoice now (no free tier)
assert invoice is not None
assert invoice.total > 0
await invoice.delete()
await UsageAggregate.filter(user=test_user).delete()

View File

@ -143,14 +143,14 @@ async def test_invoice_line_item_creation(test_user):
async def test_pricing_config_creation(test_user):
config = await PricingConfig.create(
config_key="storage_per_gb_month",
config_value=Decimal("0.0045"),
config_value=Decimal("0.005"),
description="Storage cost per GB per month",
unit="per_gb_month",
updated_by=test_user,
)
assert config.config_key == "storage_per_gb_month"
assert config.config_value == Decimal("0.0045")
assert config.config_value == Decimal("0.005")
await config.delete()