Update.
This commit is contained in:
parent
5cffab48c7
commit
7b21581108
10
Makefile
10
Makefile
@ -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: \
|
||||
|
||||
@ -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"]))
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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,
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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 %}
|
||||
@ -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()
|
||||
|
||||
@ -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()
|
||||
|
||||
|
||||
|
||||
@ -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()
|
||||
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user