billing-automation
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseBilling Automation
自动化计费
Master automated billing systems including recurring billing, invoice generation, dunning management, proration, and tax calculation.
掌握自动化计费系统,包括定期计费、发票生成、催款管理、按比例计费以及税费计算。
When to Use This Skill
适用场景
- Implementing SaaS subscription billing
- Automating invoice generation and delivery
- Managing failed payment recovery (dunning)
- Calculating prorated charges for plan changes
- Handling sales tax, VAT, and GST
- Processing usage-based billing
- Managing billing cycles and renewals
- 实施SaaS订阅计费
- 自动化发票生成与交付
- 管理失败付款的追回(催款)
- 计算套餐变更时的按比例收费
- 处理销售税、VAT和GST
- 处理基于使用量的计费
- 管理计费周期与续订
Core Concepts
核心概念
1. Billing Cycles
1. 计费周期
Common Intervals:
- Monthly (most common for SaaS)
- Annual (discounted long-term)
- Quarterly
- Weekly
- Custom (usage-based, per-seat)
常见间隔:
- 月度(SaaS最常用)
- 年度(长期订阅享折扣)
- 季度
- 周度
- 自定义(基于使用量、按席位)
2. Subscription States
2. 订阅状态
trial → active → past_due → canceled
→ paused → resumedtrial → active → past_due → canceled
→ paused → resumed3. Dunning Management
3. 催款管理
Automated process to recover failed payments through:
- Retry schedules
- Customer notifications
- Grace periods
- Account restrictions
通过以下方式自动追回失败付款的流程:
- 重试计划
- 客户通知
- 宽限期
- 账户限制
4. Proration
4. 按比例计费
Adjusting charges when:
- Upgrading/downgrading mid-cycle
- Adding/removing seats
- Changing billing frequency
在以下场景调整收费:
- 周期内升级/降级套餐
- 添加/移除席位
- 变更计费频率
Quick Start
快速开始
python
from billing import BillingEngine, Subscriptionpython
from billing import BillingEngine, SubscriptionInitialize billing engine
Initialize billing engine
billing = BillingEngine()
billing = BillingEngine()
Create subscription
Create subscription
subscription = billing.create_subscription(
customer_id="cus_123",
plan_id="plan_pro_monthly",
billing_cycle_anchor=datetime.now(),
trial_days=14
)
subscription = billing.create_subscription(
customer_id="cus_123",
plan_id="plan_pro_monthly",
billing_cycle_anchor=datetime.now(),
trial_days=14
)
Process billing cycle
Process billing cycle
billing.process_billing_cycle(subscription.id)
undefinedbilling.process_billing_cycle(subscription.id)
undefinedSubscription Lifecycle Management
订阅生命周期管理
python
from datetime import datetime, timedelta
from enum import Enum
class SubscriptionStatus(Enum):
TRIAL = "trial"
ACTIVE = "active"
PAST_DUE = "past_due"
CANCELED = "canceled"
PAUSED = "paused"
class Subscription:
def __init__(self, customer_id, plan, billing_cycle_day=None):
self.id = generate_id()
self.customer_id = customer_id
self.plan = plan
self.status = SubscriptionStatus.TRIAL
self.current_period_start = datetime.now()
self.current_period_end = self.current_period_start + timedelta(days=plan.trial_days or 30)
self.billing_cycle_day = billing_cycle_day or self.current_period_start.day
self.trial_end = datetime.now() + timedelta(days=plan.trial_days) if plan.trial_days else None
def start_trial(self, trial_days):
"""Start trial period."""
self.status = SubscriptionStatus.TRIAL
self.trial_end = datetime.now() + timedelta(days=trial_days)
self.current_period_end = self.trial_end
def activate(self):
"""Activate subscription after trial or immediately."""
self.status = SubscriptionStatus.ACTIVE
self.current_period_start = datetime.now()
self.current_period_end = self.calculate_next_billing_date()
def mark_past_due(self):
"""Mark subscription as past due after failed payment."""
self.status = SubscriptionStatus.PAST_DUE
# Trigger dunning workflow
def cancel(self, at_period_end=True):
"""Cancel subscription."""
if at_period_end:
self.cancel_at_period_end = True
# Will cancel when current period ends
else:
self.status = SubscriptionStatus.CANCELED
self.canceled_at = datetime.now()
def calculate_next_billing_date(self):
"""Calculate next billing date based on interval."""
if self.plan.interval == 'month':
return self.current_period_start + timedelta(days=30)
elif self.plan.interval == 'year':
return self.current_period_start + timedelta(days=365)
elif self.plan.interval == 'week':
return self.current_period_start + timedelta(days=7)python
from datetime import datetime, timedelta
from enum import Enum
class SubscriptionStatus(Enum):
TRIAL = "trial"
ACTIVE = "active"
PAST_DUE = "past_due"
CANCELED = "canceled"
PAUSED = "paused"
class Subscription:
def __init__(self, customer_id, plan, billing_cycle_day=None):
self.id = generate_id()
self.customer_id = customer_id
self.plan = plan
self.status = SubscriptionStatus.TRIAL
self.current_period_start = datetime.now()
self.current_period_end = self.current_period_start + timedelta(days=plan.trial_days or 30)
self.billing_cycle_day = billing_cycle_day or self.current_period_start.day
self.trial_end = datetime.now() + timedelta(days=plan.trial_days) if plan.trial_days else None
def start_trial(self, trial_days):
"""Start trial period."""
self.status = SubscriptionStatus.TRIAL
self.trial_end = datetime.now() + timedelta(days=trial_days)
self.current_period_end = self.trial_end
def activate(self):
"""Activate subscription after trial or immediately."""
self.status = SubscriptionStatus.ACTIVE
self.current_period_start = datetime.now()
self.current_period_end = self.calculate_next_billing_date()
def mark_past_due(self):
"""Mark subscription as past due after failed payment."""
self.status = SubscriptionStatus.PAST_DUE
# Trigger dunning workflow
def cancel(self, at_period_end=True):
"""Cancel subscription."""
if at_period_end:
self.cancel_at_period_end = True
# Will cancel when current period ends
else:
self.status = SubscriptionStatus.CANCELED
self.canceled_at = datetime.now()
def calculate_next_billing_date(self):
"""Calculate next billing date based on interval."""
if self.plan.interval == 'month':
return self.current_period_start + timedelta(days=30)
elif self.plan.interval == 'year':
return self.current_period_start + timedelta(days=365)
elif self.plan.interval == 'week':
return self.current_period_start + timedelta(days=7)Billing Cycle Processing
计费周期处理
python
class BillingEngine:
def process_billing_cycle(self, subscription_id):
"""Process billing for a subscription."""
subscription = self.get_subscription(subscription_id)
# Check if billing is due
if datetime.now() < subscription.current_period_end:
return
# Generate invoice
invoice = self.generate_invoice(subscription)
# Attempt payment
payment_result = self.charge_customer(
subscription.customer_id,
invoice.total
)
if payment_result.success:
# Payment successful
invoice.mark_paid()
subscription.advance_billing_period()
self.send_invoice(invoice)
else:
# Payment failed
subscription.mark_past_due()
self.start_dunning_process(subscription, invoice)
def generate_invoice(self, subscription):
"""Generate invoice for billing period."""
invoice = Invoice(
customer_id=subscription.customer_id,
subscription_id=subscription.id,
period_start=subscription.current_period_start,
period_end=subscription.current_period_end
)
# Add subscription line item
invoice.add_line_item(
description=subscription.plan.name,
amount=subscription.plan.amount,
quantity=subscription.quantity or 1
)
# Add usage-based charges if applicable
if subscription.has_usage_billing:
usage_charges = self.calculate_usage_charges(subscription)
invoice.add_line_item(
description="Usage charges",
amount=usage_charges
)
# Calculate tax
tax = self.calculate_tax(invoice.subtotal, subscription.customer)
invoice.tax = tax
invoice.finalize()
return invoice
def charge_customer(self, customer_id, amount):
"""Charge customer using saved payment method."""
customer = self.get_customer(customer_id)
try:
# Charge using payment processor
charge = stripe.Charge.create(
customer=customer.stripe_id,
amount=int(amount * 100), # Convert to cents
currency='usd'
)
return PaymentResult(success=True, transaction_id=charge.id)
except stripe.error.CardError as e:
return PaymentResult(success=False, error=str(e))python
class BillingEngine:
def process_billing_cycle(self, subscription_id):
"""Process billing for a subscription."""
subscription = self.get_subscription(subscription_id)
# Check if billing is due
if datetime.now() < subscription.current_period_end:
return
# Generate invoice
invoice = self.generate_invoice(subscription)
# Attempt payment
payment_result = self.charge_customer(
subscription.customer_id,
invoice.total
)
if payment_result.success:
# Payment successful
invoice.mark_paid()
subscription.advance_billing_period()
self.send_invoice(invoice)
else:
# Payment failed
subscription.mark_past_due()
self.start_dunning_process(subscription, invoice)
def generate_invoice(self, subscription):
"""Generate invoice for billing period."""
invoice = Invoice(
customer_id=subscription.customer_id,
subscription_id=subscription.id,
period_start=subscription.current_period_start,
period_end=subscription.current_period_end
)
# Add subscription line item
invoice.add_line_item(
description=subscription.plan.name,
amount=subscription.plan.amount,
quantity=subscription.quantity or 1
)
# Add usage-based charges if applicable
if subscription.has_usage_billing:
usage_charges = self.calculate_usage_charges(subscription)
invoice.add_line_item(
description="Usage charges",
amount=usage_charges
)
# Calculate tax
tax = self.calculate_tax(invoice.subtotal, subscription.customer)
invoice.tax = tax
invoice.finalize()
return invoice
def charge_customer(self, customer_id, amount):
"""Charge customer using saved payment method."""
customer = self.get_customer(customer_id)
try:
# Charge using payment processor
charge = stripe.Charge.create(
customer=customer.stripe_id,
amount=int(amount * 100), # Convert to cents
currency='usd'
)
return PaymentResult(success=True, transaction_id=charge.id)
except stripe.error.CardError as e:
return PaymentResult(success=False, error=str(e))Dunning Management
催款管理
python
class DunningManager:
"""Manage failed payment recovery."""
def __init__(self):
self.retry_schedule = [
{'days': 3, 'email_template': 'payment_failed_first'},
{'days': 7, 'email_template': 'payment_failed_reminder'},
{'days': 14, 'email_template': 'payment_failed_final'}
]
def start_dunning_process(self, subscription, invoice):
"""Start dunning process for failed payment."""
dunning_attempt = DunningAttempt(
subscription_id=subscription.id,
invoice_id=invoice.id,
attempt_number=1,
next_retry=datetime.now() + timedelta(days=3)
)
# Send initial failure notification
self.send_dunning_email(subscription, 'payment_failed_first')
# Schedule retries
self.schedule_retries(dunning_attempt)
def retry_payment(self, dunning_attempt):
"""Retry failed payment."""
subscription = self.get_subscription(dunning_attempt.subscription_id)
invoice = self.get_invoice(dunning_attempt.invoice_id)
# Attempt payment again
result = self.charge_customer(subscription.customer_id, invoice.total)
if result.success:
# Payment succeeded
invoice.mark_paid()
subscription.status = SubscriptionStatus.ACTIVE
self.send_dunning_email(subscription, 'payment_recovered')
dunning_attempt.mark_resolved()
else:
# Still failing
dunning_attempt.attempt_number += 1
if dunning_attempt.attempt_number < len(self.retry_schedule):
# Schedule next retry
next_retry_config = self.retry_schedule[dunning_attempt.attempt_number]
dunning_attempt.next_retry = datetime.now() + timedelta(days=next_retry_config['days'])
self.send_dunning_email(subscription, next_retry_config['email_template'])
else:
# Exhausted retries, cancel subscription
subscription.cancel(at_period_end=False)
self.send_dunning_email(subscription, 'subscription_canceled')
def send_dunning_email(self, subscription, template):
"""Send dunning notification to customer."""
customer = self.get_customer(subscription.customer_id)
email_content = self.render_template(template, {
'customer_name': customer.name,
'amount_due': subscription.plan.amount,
'update_payment_url': f"https://app.example.com/billing"
})
send_email(
to=customer.email,
subject=email_content['subject'],
body=email_content['body']
)python
class DunningManager:
"""Manage failed payment recovery."""
def __init__(self):
self.retry_schedule = [
{'days': 3, 'email_template': 'payment_failed_first'},
{'days': 7, 'email_template': 'payment_failed_reminder'},
{'days': 14, 'email_template': 'payment_failed_final'}
]
def start_dunning_process(self, subscription, invoice):
"""Start dunning process for failed payment."""
dunning_attempt = DunningAttempt(
subscription_id=subscription.id,
invoice_id=invoice.id,
attempt_number=1,
next_retry=datetime.now() + timedelta(days=3)
)
# Send initial failure notification
self.send_dunning_email(subscription, 'payment_failed_first')
# Schedule retries
self.schedule_retries(dunning_attempt)
def retry_payment(self, dunning_attempt):
"""Retry failed payment."""
subscription = self.get_subscription(dunning_attempt.subscription_id)
invoice = self.get_invoice(dunning_attempt.invoice_id)
# Attempt payment again
result = self.charge_customer(subscription.customer_id, invoice.total)
if result.success:
# Payment succeeded
invoice.mark_paid()
subscription.status = SubscriptionStatus.ACTIVE
self.send_dunning_email(subscription, 'payment_recovered')
dunning_attempt.mark_resolved()
else:
# Still failing
dunning_attempt.attempt_number += 1
if dunning_attempt.attempt_number < len(self.retry_schedule):
# Schedule next retry
next_retry_config = self.retry_schedule[dunning_attempt.attempt_number]
dunning_attempt.next_retry = datetime.now() + timedelta(days=next_retry_config['days'])
self.send_dunning_email(subscription, next_retry_config['email_template'])
else:
# Exhausted retries, cancel subscription
subscription.cancel(at_period_end=False)
self.send_dunning_email(subscription, 'subscription_canceled')
def send_dunning_email(self, subscription, template):
"""Send dunning notification to customer."""
customer = self.get_customer(subscription.customer_id)
email_content = self.render_template(template, {
'customer_name': customer.name,
'amount_due': subscription.plan.amount,
'update_payment_url': f"https://app.example.com/billing"
})
send_email(
to=customer.email,
subject=email_content['subject'],
body=email_content['body']
)Proration
按比例计费
python
class ProrationCalculator:
"""Calculate prorated charges for plan changes."""
@staticmethod
def calculate_proration(old_plan, new_plan, period_start, period_end, change_date):
"""Calculate proration for plan change."""
# Days in current period
total_days = (period_end - period_start).days
# Days used on old plan
days_used = (change_date - period_start).days
# Days remaining on new plan
days_remaining = (period_end - change_date).days
# Calculate prorated amounts
unused_amount = (old_plan.amount / total_days) * days_remaining
new_plan_amount = (new_plan.amount / total_days) * days_remaining
# Net charge/credit
proration = new_plan_amount - unused_amount
return {
'old_plan_credit': -unused_amount,
'new_plan_charge': new_plan_amount,
'net_proration': proration,
'days_used': days_used,
'days_remaining': days_remaining
}
@staticmethod
def calculate_seat_proration(current_seats, new_seats, price_per_seat, period_start, period_end, change_date):
"""Calculate proration for seat changes."""
total_days = (period_end - period_start).days
days_remaining = (period_end - change_date).days
# Additional seats charge
additional_seats = new_seats - current_seats
prorated_amount = (additional_seats * price_per_seat / total_days) * days_remaining
return {
'additional_seats': additional_seats,
'prorated_charge': max(0, prorated_amount), # No refund for removing seats mid-cycle
'effective_date': change_date
}python
class ProrationCalculator:
"""Calculate prorated charges for plan changes."""
@staticmethod
def calculate_proration(old_plan, new_plan, period_start, period_end, change_date):
"""Calculate proration for plan change."""
# Days in current period
total_days = (period_end - period_start).days
# Days used on old plan
days_used = (change_date - period_start).days
# Days remaining on new plan
days_remaining = (period_end - change_date).days
# Calculate prorated amounts
unused_amount = (old_plan.amount / total_days) * days_remaining
new_plan_amount = (new_plan.amount / total_days) * days_remaining
# Net charge/credit
proration = new_plan_amount - unused_amount
return {
'old_plan_credit': -unused_amount,
'new_plan_charge': new_plan_amount,
'net_proration': proration,
'days_used': days_used,
'days_remaining': days_remaining
}
@staticmethod
def calculate_seat_proration(current_seats, new_seats, price_per_seat, period_start, period_end, change_date):
"""Calculate proration for seat changes."""
total_days = (period_end - period_start).days
days_remaining = (period_end - change_date).days
# Additional seats charge
additional_seats = new_seats - current_seats
prorated_amount = (additional_seats * price_per_seat / total_days) * days_remaining
return {
'additional_seats': additional_seats,
'prorated_charge': max(0, prorated_amount), # No refund for removing seats mid-cycle
'effective_date': change_date
}Tax Calculation
税费计算
python
class TaxCalculator:
"""Calculate sales tax, VAT, GST."""
def __init__(self):
# Tax rates by region
self.tax_rates = {
'US_CA': 0.0725, # California sales tax
'US_NY': 0.04, # New York sales tax
'GB': 0.20, # UK VAT
'DE': 0.19, # Germany VAT
'FR': 0.20, # France VAT
'AU': 0.10, # Australia GST
}
def calculate_tax(self, amount, customer):
"""Calculate applicable tax."""
# Determine tax jurisdiction
jurisdiction = self.get_tax_jurisdiction(customer)
if not jurisdiction:
return 0
# Get tax rate
tax_rate = self.tax_rates.get(jurisdiction, 0)
# Calculate tax
tax = amount * tax_rate
return {
'tax_amount': tax,
'tax_rate': tax_rate,
'jurisdiction': jurisdiction,
'tax_type': self.get_tax_type(jurisdiction)
}
def get_tax_jurisdiction(self, customer):
"""Determine tax jurisdiction based on customer location."""
if customer.country == 'US':
# US: Tax based on customer state
return f"US_{customer.state}"
elif customer.country in ['GB', 'DE', 'FR']:
# EU: VAT
return customer.country
elif customer.country == 'AU':
# Australia: GST
return 'AU'
else:
return None
def get_tax_type(self, jurisdiction):
"""Get type of tax for jurisdiction."""
if jurisdiction.startswith('US_'):
return 'Sales Tax'
elif jurisdiction in ['GB', 'DE', 'FR']:
return 'VAT'
elif jurisdiction == 'AU':
return 'GST'
return 'Tax'
def validate_vat_number(self, vat_number, country):
"""Validate EU VAT number."""
# Use VIES API for validation
# Returns True if valid, False otherwise
passpython
class TaxCalculator:
"""Calculate sales tax, VAT, GST."""
def __init__(self):
# Tax rates by region
self.tax_rates = {
'US_CA': 0.0725, # California sales tax
'US_NY': 0.04, # New York sales tax
'GB': 0.20, # UK VAT
'DE': 0.19, # Germany VAT
'FR': 0.20, # France VAT
'AU': 0.10, # Australia GST
}
def calculate_tax(self, amount, customer):
"""Calculate applicable tax."""
# Determine tax jurisdiction
jurisdiction = self.get_tax_jurisdiction(customer)
if not jurisdiction:
return 0
# Get tax rate
tax_rate = self.tax_rates.get(jurisdiction, 0)
# Calculate tax
tax = amount * tax_rate
return {
'tax_amount': tax,
'tax_rate': tax_rate,
'jurisdiction': jurisdiction,
'tax_type': self.get_tax_type(jurisdiction)
}
def get_tax_jurisdiction(self, customer):
"""Determine tax jurisdiction based on customer location."""
if customer.country == 'US':
# US: Tax based on customer state
return f"US_{customer.state}"
elif customer.country in ['GB', 'DE', 'FR']:
# EU: VAT
return customer.country
elif customer.country == 'AU':
# Australia: GST
return 'AU'
else:
return None
def get_tax_type(self, jurisdiction):
"""Get type of tax for jurisdiction."""
if jurisdiction.startswith('US_'):
return 'Sales Tax'
elif jurisdiction in ['GB', 'DE', 'FR']:
return 'VAT'
elif jurisdiction == 'AU':
return 'GST'
return 'Tax'
def validate_vat_number(self, vat_number, country):
"""Validate EU VAT number."""
# Use VIES API for validation
# Returns True if valid, False otherwise
passInvoice Generation
发票生成
python
class Invoice:
def __init__(self, customer_id, subscription_id=None):
self.id = generate_invoice_number()
self.customer_id = customer_id
self.subscription_id = subscription_id
self.status = 'draft'
self.line_items = []
self.subtotal = 0
self.tax = 0
self.total = 0
self.created_at = datetime.now()
def add_line_item(self, description, amount, quantity=1):
"""Add line item to invoice."""
line_item = {
'description': description,
'unit_amount': amount,
'quantity': quantity,
'total': amount * quantity
}
self.line_items.append(line_item)
self.subtotal += line_item['total']
def finalize(self):
"""Finalize invoice and calculate total."""
self.total = self.subtotal + self.tax
self.status = 'open'
self.finalized_at = datetime.now()
def mark_paid(self):
"""Mark invoice as paid."""
self.status = 'paid'
self.paid_at = datetime.now()
def to_pdf(self):
"""Generate PDF invoice."""
from reportlab.pdfgen import canvas
# Generate PDF
# Include: company info, customer info, line items, tax, total
pass
def to_html(self):
"""Generate HTML invoice."""
template = """
<!DOCTYPE html>
<html>
<head><title>Invoice #{invoice_number}</title></head>
<body>
<h1>Invoice #{invoice_number}</h1>
<p>Date: {date}</p>
<h2>Bill To:</h2>
<p>{customer_name}<br>{customer_address}</p>
<table>
<tr><th>Description</th><th>Quantity</th><th>Amount</th></tr>
{line_items}
</table>
<p>Subtotal: ${subtotal}</p>
<p>Tax: ${tax}</p>
<h3>Total: ${total}</h3>
</body>
</html>
"""
return template.format(
invoice_number=self.id,
date=self.created_at.strftime('%Y-%m-%d'),
customer_name=self.customer.name,
customer_address=self.customer.address,
line_items=self.render_line_items(),
subtotal=self.subtotal,
tax=self.tax,
total=self.total
)python
class Invoice:
def __init__(self, customer_id, subscription_id=None):
self.id = generate_invoice_number()
self.customer_id = customer_id
self.subscription_id = subscription_id
self.status = 'draft'
self.line_items = []
self.subtotal = 0
self.tax = 0
self.total = 0
self.created_at = datetime.now()
def add_line_item(self, description, amount, quantity=1):
"""Add line item to invoice."""
line_item = {
'description': description,
'unit_amount': amount,
'quantity': quantity,
'total': amount * quantity
}
self.line_items.append(line_item)
self.subtotal += line_item['total']
def finalize(self):
"""Finalize invoice and calculate total."""
self.total = self.subtotal + self.tax
self.status = 'open'
self.finalized_at = datetime.now()
def mark_paid(self):
"""Mark invoice as paid."""
self.status = 'paid'
self.paid_at = datetime.now()
def to_pdf(self):
"""Generate PDF invoice."""
from reportlab.pdfgen import canvas
# Generate PDF
# Include: company info, customer info, line items, tax, total
pass
def to_html(self):
"""Generate HTML invoice."""
template = """
<!DOCTYPE html>
<html>
<head><title>Invoice #{invoice_number}</title></head>
<body>
<h1>Invoice #{invoice_number}</h1>
<p>Date: {date}</p>
<h2>Bill To:</h2>
<p>{customer_name}<br>{customer_address}</p>
<table>
<tr><th>Description</th><th>Quantity</th><th>Amount</th></tr>
{line_items}
</table>
<p>Subtotal: ${subtotal}</p>
<p>Tax: ${tax}</p>
<h3>Total: ${total}</h3>
</body>
</html>
"""
return template.format(
invoice_number=self.id,
date=self.created_at.strftime('%Y-%m-%d'),
customer_name=self.customer.name,
customer_address=self.customer.address,
line_items=self.render_line_items(),
subtotal=self.subtotal,
tax=self.tax,
total=self.total
)Usage-Based Billing
基于使用量的计费
python
class UsageBillingEngine:
"""Track and bill for usage."""
def track_usage(self, customer_id, metric, quantity):
"""Track usage event."""
UsageRecord.create(
customer_id=customer_id,
metric=metric,
quantity=quantity,
timestamp=datetime.now()
)
def calculate_usage_charges(self, subscription, period_start, period_end):
"""Calculate charges for usage in billing period."""
usage_records = UsageRecord.get_for_period(
subscription.customer_id,
period_start,
period_end
)
total_usage = sum(record.quantity for record in usage_records)
# Tiered pricing
if subscription.plan.pricing_model == 'tiered':
charge = self.calculate_tiered_pricing(total_usage, subscription.plan.tiers)
# Per-unit pricing
elif subscription.plan.pricing_model == 'per_unit':
charge = total_usage * subscription.plan.unit_price
# Volume pricing
elif subscription.plan.pricing_model == 'volume':
charge = self.calculate_volume_pricing(total_usage, subscription.plan.tiers)
return charge
def calculate_tiered_pricing(self, total_usage, tiers):
"""Calculate cost using tiered pricing."""
charge = 0
remaining = total_usage
for tier in sorted(tiers, key=lambda x: x['up_to']):
tier_usage = min(remaining, tier['up_to'] - tier['from'])
charge += tier_usage * tier['unit_price']
remaining -= tier_usage
if remaining <= 0:
break
return chargepython
class UsageBillingEngine:
"""Track and bill for usage."""
def track_usage(self, customer_id, metric, quantity):
"""Track usage event."""
UsageRecord.create(
customer_id=customer_id,
metric=metric,
quantity=quantity,
timestamp=datetime.now()
)
def calculate_usage_charges(self, subscription, period_start, period_end):
"""Calculate charges for usage in billing period."""
usage_records = UsageRecord.get_for_period(
subscription.customer_id,
period_start,
period_end
)
total_usage = sum(record.quantity for record in usage_records)
# Tiered pricing
if subscription.plan.pricing_model == 'tiered':
charge = self.calculate_tiered_pricing(total_usage, subscription.plan.tiers)
# Per-unit pricing
elif subscription.plan.pricing_model == 'per_unit':
charge = total_usage * subscription.plan.unit_price
# Volume pricing
elif subscription.plan.pricing_model == 'volume':
charge = self.calculate_volume_pricing(total_usage, subscription.plan.tiers)
return charge
def calculate_tiered_pricing(self, total_usage, tiers):
"""Calculate cost using tiered pricing."""
charge = 0
remaining = total_usage
for tier in sorted(tiers, key=lambda x: x['up_to']):
tier_usage = min(remaining, tier['up_to'] - tier['from'])
charge += tier_usage * tier['unit_price']
remaining -= tier_usage
if remaining <= 0:
break
return chargeResources
资源
- references/billing-cycles.md: Billing cycle management
- references/dunning-management.md: Failed payment recovery
- references/proration.md: Prorated charge calculations
- references/tax-calculation.md: Tax/VAT/GST handling
- references/invoice-lifecycle.md: Invoice state management
- assets/billing-state-machine.yaml: Billing workflow
- assets/invoice-template.html: Invoice templates
- assets/dunning-policy.yaml: Dunning configuration
- references/billing-cycles.md: 计费周期管理
- references/dunning-management.md: 失败付款追回
- references/proration.md: 按比例收费计算
- references/tax-calculation.md: 税/VAT/GST处理
- references/invoice-lifecycle.md: 发票状态管理
- assets/billing-state-machine.yaml: 计费工作流
- assets/invoice-template.html: 发票模板
- assets/dunning-policy.yaml: 催款配置
Best Practices
最佳实践
- Automate Everything: Minimize manual intervention
- Clear Communication: Notify customers of billing events
- Flexible Retry Logic: Balance recovery with customer experience
- Accurate Proration: Fair calculation for plan changes
- Tax Compliance: Calculate correct tax for jurisdiction
- Audit Trail: Log all billing events
- Graceful Degradation: Handle edge cases without breaking
- 全面自动化:尽量减少人工干预
- 清晰沟通:向客户通知计费事件
- 灵活重试逻辑:平衡付款追回与客户体验
- 准确按比例计费:公平计算套餐变更的费用
- 税务合规:针对不同地区计算正确税费
- 审计追踪:记录所有计费事件
- 优雅降级:处理边缘情况时避免系统崩溃
Common Pitfalls
常见陷阱
- Incorrect Proration: Not accounting for partial periods
- Missing Tax: Forgetting to add tax to invoices
- Aggressive Dunning: Canceling too quickly
- No Notifications: Not informing customers of failures
- Hardcoded Cycles: Not supporting custom billing dates
- 按比例计费错误:未考虑部分周期
- 遗漏税费:忘记在发票中添加税费
- 催款过于激进:过快取消订阅
- 缺少通知:未告知客户付款失败
- 硬编码周期:不支持自定义计费日期