This commit is contained in:
retoor 2026-01-22 16:40:16 +01:00
parent 5cffab48c7
commit 7b21581108
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()