specweave 0.3.13 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CLAUDE.md +17 -1
- package/README.md +1 -1
- package/bin/install-all.sh +9 -2
- package/bin/install-hooks.sh +57 -0
- package/dist/cli/commands/init.d.ts.map +1 -1
- package/dist/cli/commands/init.js +55 -0
- package/dist/cli/commands/init.js.map +1 -1
- package/dist/core/agent-model-manager.d.ts +52 -0
- package/dist/core/agent-model-manager.d.ts.map +1 -0
- package/dist/core/agent-model-manager.js +120 -0
- package/dist/core/agent-model-manager.js.map +1 -0
- package/dist/core/cost-tracker.d.ts +108 -0
- package/dist/core/cost-tracker.d.ts.map +1 -0
- package/dist/core/cost-tracker.js +281 -0
- package/dist/core/cost-tracker.js.map +1 -0
- package/dist/core/model-selector.d.ts +57 -0
- package/dist/core/model-selector.d.ts.map +1 -0
- package/dist/core/model-selector.js +115 -0
- package/dist/core/model-selector.js.map +1 -0
- package/dist/core/phase-detector.d.ts +62 -0
- package/dist/core/phase-detector.d.ts.map +1 -0
- package/dist/core/phase-detector.js +229 -0
- package/dist/core/phase-detector.js.map +1 -0
- package/dist/types/cost-tracking.d.ts +43 -0
- package/dist/types/cost-tracking.d.ts.map +1 -0
- package/dist/types/cost-tracking.js +8 -0
- package/dist/types/cost-tracking.js.map +1 -0
- package/dist/types/model-selection.d.ts +53 -0
- package/dist/types/model-selection.d.ts.map +1 -0
- package/dist/types/model-selection.js +12 -0
- package/dist/types/model-selection.js.map +1 -0
- package/dist/utils/cost-reporter.d.ts +58 -0
- package/dist/utils/cost-reporter.d.ts.map +1 -0
- package/dist/utils/cost-reporter.js +224 -0
- package/dist/utils/cost-reporter.js.map +1 -0
- package/dist/utils/pricing-constants.d.ts +70 -0
- package/dist/utils/pricing-constants.d.ts.map +1 -0
- package/dist/utils/pricing-constants.js +71 -0
- package/dist/utils/pricing-constants.js.map +1 -0
- package/package.json +1 -1
- package/src/agents/architect/AGENT.md +3 -0
- package/src/agents/code-reviewer.md +156 -0
- package/src/agents/data-scientist/AGENT.md +181 -0
- package/src/agents/database-optimizer/AGENT.md +147 -0
- package/src/agents/devops/AGENT.md +3 -0
- package/src/agents/diagrams-architect/AGENT.md +3 -0
- package/src/agents/docs-writer/AGENT.md +3 -0
- package/src/agents/kubernetes-architect/AGENT.md +142 -0
- package/src/agents/ml-engineer/AGENT.md +150 -0
- package/src/agents/mlops-engineer/AGENT.md +201 -0
- package/src/agents/network-engineer/AGENT.md +149 -0
- package/src/agents/observability-engineer/AGENT.md +213 -0
- package/src/agents/payment-integration/AGENT.md +35 -0
- package/src/agents/performance/AGENT.md +3 -0
- package/src/agents/performance-engineer/AGENT.md +153 -0
- package/src/agents/pm/AGENT.md +3 -0
- package/src/agents/qa-lead/AGENT.md +3 -0
- package/src/agents/security/AGENT.md +3 -0
- package/src/agents/sre/AGENT.md +3 -0
- package/src/agents/tdd-orchestrator/AGENT.md +169 -0
- package/src/agents/tech-lead/AGENT.md +3 -0
- package/src/commands/specweave.costs.md +261 -0
- package/src/commands/specweave.ml-pipeline.md +292 -0
- package/src/commands/specweave.monitor-setup.md +501 -0
- package/src/commands/specweave.slo-implement.md +1055 -0
- package/src/commands/specweave.sync-github.md +1 -1
- package/src/commands/specweave.tdd-cycle.md +199 -0
- package/src/commands/specweave.tdd-green.md +842 -0
- package/src/commands/specweave.tdd-red.md +135 -0
- package/src/commands/specweave.tdd-refactor.md +165 -0
- package/src/skills/SKILLS-INDEX.md +18 -10
- package/src/skills/billing-automation/SKILL.md +559 -0
- package/src/skills/distributed-tracing/SKILL.md +438 -0
- package/src/skills/e2e-playwright/README.md +1 -1
- package/src/skills/e2e-playwright/package.json +1 -1
- package/src/skills/gitops-workflow/SKILL.md +285 -0
- package/src/skills/gitops-workflow/references/argocd-setup.md +134 -0
- package/src/skills/gitops-workflow/references/sync-policies.md +131 -0
- package/src/skills/grafana-dashboards/SKILL.md +369 -0
- package/src/skills/helm-chart-scaffolding/SKILL.md +544 -0
- package/src/skills/helm-chart-scaffolding/assets/Chart.yaml.template +42 -0
- package/src/skills/helm-chart-scaffolding/assets/values.yaml.template +185 -0
- package/src/skills/helm-chart-scaffolding/references/chart-structure.md +500 -0
- package/src/skills/helm-chart-scaffolding/scripts/validate-chart.sh +244 -0
- package/src/skills/k8s-manifest-generator/SKILL.md +511 -0
- package/src/skills/k8s-manifest-generator/assets/configmap-template.yaml +296 -0
- package/src/skills/k8s-manifest-generator/assets/deployment-template.yaml +203 -0
- package/src/skills/k8s-manifest-generator/assets/service-template.yaml +171 -0
- package/src/skills/k8s-manifest-generator/references/deployment-spec.md +753 -0
- package/src/skills/k8s-manifest-generator/references/service-spec.md +724 -0
- package/src/skills/k8s-security-policies/SKILL.md +334 -0
- package/src/skills/k8s-security-policies/assets/network-policy-template.yaml +177 -0
- package/src/skills/k8s-security-policies/references/rbac-patterns.md +187 -0
- package/src/skills/ml-pipeline-workflow/SKILL.md +245 -0
- package/src/skills/paypal-integration/SKILL.md +467 -0
- package/src/skills/pci-compliance/SKILL.md +466 -0
- package/src/skills/prometheus-configuration/SKILL.md +392 -0
- package/src/skills/slo-implementation/SKILL.md +329 -0
- package/src/skills/stripe-integration/SKILL.md +442 -0
- package/src/skills/tdd-workflow/SKILL.md +378 -0
- package/src/templates/README.md.template +1 -1
- package/src/skills/bmad-method-expert/SKILL.md +0 -626
- package/src/skills/bmad-method-expert/scripts/analyze-project.js +0 -318
- package/src/skills/bmad-method-expert/scripts/check-setup.js +0 -208
- package/src/skills/bmad-method-expert/scripts/generate-template.js +0 -1149
- package/src/skills/bmad-method-expert/scripts/validate-documents.js +0 -340
- package/src/skills/context-optimizer/SKILL.md +0 -588
- package/src/skills/figma-designer/SKILL.md +0 -149
- package/src/skills/figma-implementer/SKILL.md +0 -148
- package/src/skills/figma-mcp-connector/SKILL.md +0 -136
- package/src/skills/figma-to-code/SKILL.md +0 -128
- package/src/skills/spec-kit-expert/SKILL.md +0 -1010
|
@@ -0,0 +1,559 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: billing-automation
|
|
3
|
+
description: Build automated billing systems for recurring payments, invoicing, subscription lifecycle, and dunning management. Use when implementing subscription billing, automating invoicing, or managing recurring payment systems.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Billing Automation
|
|
7
|
+
|
|
8
|
+
Master automated billing systems including recurring billing, invoice generation, dunning management, proration, and tax calculation.
|
|
9
|
+
|
|
10
|
+
## When to Use This Skill
|
|
11
|
+
|
|
12
|
+
- Implementing SaaS subscription billing
|
|
13
|
+
- Automating invoice generation and delivery
|
|
14
|
+
- Managing failed payment recovery (dunning)
|
|
15
|
+
- Calculating prorated charges for plan changes
|
|
16
|
+
- Handling sales tax, VAT, and GST
|
|
17
|
+
- Processing usage-based billing
|
|
18
|
+
- Managing billing cycles and renewals
|
|
19
|
+
|
|
20
|
+
## Core Concepts
|
|
21
|
+
|
|
22
|
+
### 1. Billing Cycles
|
|
23
|
+
**Common Intervals:**
|
|
24
|
+
- Monthly (most common for SaaS)
|
|
25
|
+
- Annual (discounted long-term)
|
|
26
|
+
- Quarterly
|
|
27
|
+
- Weekly
|
|
28
|
+
- Custom (usage-based, per-seat)
|
|
29
|
+
|
|
30
|
+
### 2. Subscription States
|
|
31
|
+
```
|
|
32
|
+
trial → active → past_due → canceled
|
|
33
|
+
→ paused → resumed
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
### 3. Dunning Management
|
|
37
|
+
Automated process to recover failed payments through:
|
|
38
|
+
- Retry schedules
|
|
39
|
+
- Customer notifications
|
|
40
|
+
- Grace periods
|
|
41
|
+
- Account restrictions
|
|
42
|
+
|
|
43
|
+
### 4. Proration
|
|
44
|
+
Adjusting charges when:
|
|
45
|
+
- Upgrading/downgrading mid-cycle
|
|
46
|
+
- Adding/removing seats
|
|
47
|
+
- Changing billing frequency
|
|
48
|
+
|
|
49
|
+
## Quick Start
|
|
50
|
+
|
|
51
|
+
```python
|
|
52
|
+
from billing import BillingEngine, Subscription
|
|
53
|
+
|
|
54
|
+
# Initialize billing engine
|
|
55
|
+
billing = BillingEngine()
|
|
56
|
+
|
|
57
|
+
# Create subscription
|
|
58
|
+
subscription = billing.create_subscription(
|
|
59
|
+
customer_id="cus_123",
|
|
60
|
+
plan_id="plan_pro_monthly",
|
|
61
|
+
billing_cycle_anchor=datetime.now(),
|
|
62
|
+
trial_days=14
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
# Process billing cycle
|
|
66
|
+
billing.process_billing_cycle(subscription.id)
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## Subscription Lifecycle Management
|
|
70
|
+
|
|
71
|
+
```python
|
|
72
|
+
from datetime import datetime, timedelta
|
|
73
|
+
from enum import Enum
|
|
74
|
+
|
|
75
|
+
class SubscriptionStatus(Enum):
|
|
76
|
+
TRIAL = "trial"
|
|
77
|
+
ACTIVE = "active"
|
|
78
|
+
PAST_DUE = "past_due"
|
|
79
|
+
CANCELED = "canceled"
|
|
80
|
+
PAUSED = "paused"
|
|
81
|
+
|
|
82
|
+
class Subscription:
|
|
83
|
+
def __init__(self, customer_id, plan, billing_cycle_day=None):
|
|
84
|
+
self.id = generate_id()
|
|
85
|
+
self.customer_id = customer_id
|
|
86
|
+
self.plan = plan
|
|
87
|
+
self.status = SubscriptionStatus.TRIAL
|
|
88
|
+
self.current_period_start = datetime.now()
|
|
89
|
+
self.current_period_end = self.current_period_start + timedelta(days=plan.trial_days or 30)
|
|
90
|
+
self.billing_cycle_day = billing_cycle_day or self.current_period_start.day
|
|
91
|
+
self.trial_end = datetime.now() + timedelta(days=plan.trial_days) if plan.trial_days else None
|
|
92
|
+
|
|
93
|
+
def start_trial(self, trial_days):
|
|
94
|
+
"""Start trial period."""
|
|
95
|
+
self.status = SubscriptionStatus.TRIAL
|
|
96
|
+
self.trial_end = datetime.now() + timedelta(days=trial_days)
|
|
97
|
+
self.current_period_end = self.trial_end
|
|
98
|
+
|
|
99
|
+
def activate(self):
|
|
100
|
+
"""Activate subscription after trial or immediately."""
|
|
101
|
+
self.status = SubscriptionStatus.ACTIVE
|
|
102
|
+
self.current_period_start = datetime.now()
|
|
103
|
+
self.current_period_end = self.calculate_next_billing_date()
|
|
104
|
+
|
|
105
|
+
def mark_past_due(self):
|
|
106
|
+
"""Mark subscription as past due after failed payment."""
|
|
107
|
+
self.status = SubscriptionStatus.PAST_DUE
|
|
108
|
+
# Trigger dunning workflow
|
|
109
|
+
|
|
110
|
+
def cancel(self, at_period_end=True):
|
|
111
|
+
"""Cancel subscription."""
|
|
112
|
+
if at_period_end:
|
|
113
|
+
self.cancel_at_period_end = True
|
|
114
|
+
# Will cancel when current period ends
|
|
115
|
+
else:
|
|
116
|
+
self.status = SubscriptionStatus.CANCELED
|
|
117
|
+
self.canceled_at = datetime.now()
|
|
118
|
+
|
|
119
|
+
def calculate_next_billing_date(self):
|
|
120
|
+
"""Calculate next billing date based on interval."""
|
|
121
|
+
if self.plan.interval == 'month':
|
|
122
|
+
return self.current_period_start + timedelta(days=30)
|
|
123
|
+
elif self.plan.interval == 'year':
|
|
124
|
+
return self.current_period_start + timedelta(days=365)
|
|
125
|
+
elif self.plan.interval == 'week':
|
|
126
|
+
return self.current_period_start + timedelta(days=7)
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
## Billing Cycle Processing
|
|
130
|
+
|
|
131
|
+
```python
|
|
132
|
+
class BillingEngine:
|
|
133
|
+
def process_billing_cycle(self, subscription_id):
|
|
134
|
+
"""Process billing for a subscription."""
|
|
135
|
+
subscription = self.get_subscription(subscription_id)
|
|
136
|
+
|
|
137
|
+
# Check if billing is due
|
|
138
|
+
if datetime.now() < subscription.current_period_end:
|
|
139
|
+
return
|
|
140
|
+
|
|
141
|
+
# Generate invoice
|
|
142
|
+
invoice = self.generate_invoice(subscription)
|
|
143
|
+
|
|
144
|
+
# Attempt payment
|
|
145
|
+
payment_result = self.charge_customer(
|
|
146
|
+
subscription.customer_id,
|
|
147
|
+
invoice.total
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
if payment_result.success:
|
|
151
|
+
# Payment successful
|
|
152
|
+
invoice.mark_paid()
|
|
153
|
+
subscription.advance_billing_period()
|
|
154
|
+
self.send_invoice(invoice)
|
|
155
|
+
else:
|
|
156
|
+
# Payment failed
|
|
157
|
+
subscription.mark_past_due()
|
|
158
|
+
self.start_dunning_process(subscription, invoice)
|
|
159
|
+
|
|
160
|
+
def generate_invoice(self, subscription):
|
|
161
|
+
"""Generate invoice for billing period."""
|
|
162
|
+
invoice = Invoice(
|
|
163
|
+
customer_id=subscription.customer_id,
|
|
164
|
+
subscription_id=subscription.id,
|
|
165
|
+
period_start=subscription.current_period_start,
|
|
166
|
+
period_end=subscription.current_period_end
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
# Add subscription line item
|
|
170
|
+
invoice.add_line_item(
|
|
171
|
+
description=subscription.plan.name,
|
|
172
|
+
amount=subscription.plan.amount,
|
|
173
|
+
quantity=subscription.quantity or 1
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
# Add usage-based charges if applicable
|
|
177
|
+
if subscription.has_usage_billing:
|
|
178
|
+
usage_charges = self.calculate_usage_charges(subscription)
|
|
179
|
+
invoice.add_line_item(
|
|
180
|
+
description="Usage charges",
|
|
181
|
+
amount=usage_charges
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
# Calculate tax
|
|
185
|
+
tax = self.calculate_tax(invoice.subtotal, subscription.customer)
|
|
186
|
+
invoice.tax = tax
|
|
187
|
+
|
|
188
|
+
invoice.finalize()
|
|
189
|
+
return invoice
|
|
190
|
+
|
|
191
|
+
def charge_customer(self, customer_id, amount):
|
|
192
|
+
"""Charge customer using saved payment method."""
|
|
193
|
+
customer = self.get_customer(customer_id)
|
|
194
|
+
|
|
195
|
+
try:
|
|
196
|
+
# Charge using payment processor
|
|
197
|
+
charge = stripe.Charge.create(
|
|
198
|
+
customer=customer.stripe_id,
|
|
199
|
+
amount=int(amount * 100), # Convert to cents
|
|
200
|
+
currency='usd'
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
return PaymentResult(success=True, transaction_id=charge.id)
|
|
204
|
+
except stripe.error.CardError as e:
|
|
205
|
+
return PaymentResult(success=False, error=str(e))
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
## Dunning Management
|
|
209
|
+
|
|
210
|
+
```python
|
|
211
|
+
class DunningManager:
|
|
212
|
+
"""Manage failed payment recovery."""
|
|
213
|
+
|
|
214
|
+
def __init__(self):
|
|
215
|
+
self.retry_schedule = [
|
|
216
|
+
{'days': 3, 'email_template': 'payment_failed_first'},
|
|
217
|
+
{'days': 7, 'email_template': 'payment_failed_reminder'},
|
|
218
|
+
{'days': 14, 'email_template': 'payment_failed_final'}
|
|
219
|
+
]
|
|
220
|
+
|
|
221
|
+
def start_dunning_process(self, subscription, invoice):
|
|
222
|
+
"""Start dunning process for failed payment."""
|
|
223
|
+
dunning_attempt = DunningAttempt(
|
|
224
|
+
subscription_id=subscription.id,
|
|
225
|
+
invoice_id=invoice.id,
|
|
226
|
+
attempt_number=1,
|
|
227
|
+
next_retry=datetime.now() + timedelta(days=3)
|
|
228
|
+
)
|
|
229
|
+
|
|
230
|
+
# Send initial failure notification
|
|
231
|
+
self.send_dunning_email(subscription, 'payment_failed_first')
|
|
232
|
+
|
|
233
|
+
# Schedule retries
|
|
234
|
+
self.schedule_retries(dunning_attempt)
|
|
235
|
+
|
|
236
|
+
def retry_payment(self, dunning_attempt):
|
|
237
|
+
"""Retry failed payment."""
|
|
238
|
+
subscription = self.get_subscription(dunning_attempt.subscription_id)
|
|
239
|
+
invoice = self.get_invoice(dunning_attempt.invoice_id)
|
|
240
|
+
|
|
241
|
+
# Attempt payment again
|
|
242
|
+
result = self.charge_customer(subscription.customer_id, invoice.total)
|
|
243
|
+
|
|
244
|
+
if result.success:
|
|
245
|
+
# Payment succeeded
|
|
246
|
+
invoice.mark_paid()
|
|
247
|
+
subscription.status = SubscriptionStatus.ACTIVE
|
|
248
|
+
self.send_dunning_email(subscription, 'payment_recovered')
|
|
249
|
+
dunning_attempt.mark_resolved()
|
|
250
|
+
else:
|
|
251
|
+
# Still failing
|
|
252
|
+
dunning_attempt.attempt_number += 1
|
|
253
|
+
|
|
254
|
+
if dunning_attempt.attempt_number < len(self.retry_schedule):
|
|
255
|
+
# Schedule next retry
|
|
256
|
+
next_retry_config = self.retry_schedule[dunning_attempt.attempt_number]
|
|
257
|
+
dunning_attempt.next_retry = datetime.now() + timedelta(days=next_retry_config['days'])
|
|
258
|
+
self.send_dunning_email(subscription, next_retry_config['email_template'])
|
|
259
|
+
else:
|
|
260
|
+
# Exhausted retries, cancel subscription
|
|
261
|
+
subscription.cancel(at_period_end=False)
|
|
262
|
+
self.send_dunning_email(subscription, 'subscription_canceled')
|
|
263
|
+
|
|
264
|
+
def send_dunning_email(self, subscription, template):
|
|
265
|
+
"""Send dunning notification to customer."""
|
|
266
|
+
customer = self.get_customer(subscription.customer_id)
|
|
267
|
+
|
|
268
|
+
email_content = self.render_template(template, {
|
|
269
|
+
'customer_name': customer.name,
|
|
270
|
+
'amount_due': subscription.plan.amount,
|
|
271
|
+
'update_payment_url': f"https://app.example.com/billing"
|
|
272
|
+
})
|
|
273
|
+
|
|
274
|
+
send_email(
|
|
275
|
+
to=customer.email,
|
|
276
|
+
subject=email_content['subject'],
|
|
277
|
+
body=email_content['body']
|
|
278
|
+
)
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
## Proration
|
|
282
|
+
|
|
283
|
+
```python
|
|
284
|
+
class ProrationCalculator:
|
|
285
|
+
"""Calculate prorated charges for plan changes."""
|
|
286
|
+
|
|
287
|
+
@staticmethod
|
|
288
|
+
def calculate_proration(old_plan, new_plan, period_start, period_end, change_date):
|
|
289
|
+
"""Calculate proration for plan change."""
|
|
290
|
+
# Days in current period
|
|
291
|
+
total_days = (period_end - period_start).days
|
|
292
|
+
|
|
293
|
+
# Days used on old plan
|
|
294
|
+
days_used = (change_date - period_start).days
|
|
295
|
+
|
|
296
|
+
# Days remaining on new plan
|
|
297
|
+
days_remaining = (period_end - change_date).days
|
|
298
|
+
|
|
299
|
+
# Calculate prorated amounts
|
|
300
|
+
unused_amount = (old_plan.amount / total_days) * days_remaining
|
|
301
|
+
new_plan_amount = (new_plan.amount / total_days) * days_remaining
|
|
302
|
+
|
|
303
|
+
# Net charge/credit
|
|
304
|
+
proration = new_plan_amount - unused_amount
|
|
305
|
+
|
|
306
|
+
return {
|
|
307
|
+
'old_plan_credit': -unused_amount,
|
|
308
|
+
'new_plan_charge': new_plan_amount,
|
|
309
|
+
'net_proration': proration,
|
|
310
|
+
'days_used': days_used,
|
|
311
|
+
'days_remaining': days_remaining
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
@staticmethod
|
|
315
|
+
def calculate_seat_proration(current_seats, new_seats, price_per_seat, period_start, period_end, change_date):
|
|
316
|
+
"""Calculate proration for seat changes."""
|
|
317
|
+
total_days = (period_end - period_start).days
|
|
318
|
+
days_remaining = (period_end - change_date).days
|
|
319
|
+
|
|
320
|
+
# Additional seats charge
|
|
321
|
+
additional_seats = new_seats - current_seats
|
|
322
|
+
prorated_amount = (additional_seats * price_per_seat / total_days) * days_remaining
|
|
323
|
+
|
|
324
|
+
return {
|
|
325
|
+
'additional_seats': additional_seats,
|
|
326
|
+
'prorated_charge': max(0, prorated_amount), # No refund for removing seats mid-cycle
|
|
327
|
+
'effective_date': change_date
|
|
328
|
+
}
|
|
329
|
+
```
|
|
330
|
+
|
|
331
|
+
## Tax Calculation
|
|
332
|
+
|
|
333
|
+
```python
|
|
334
|
+
class TaxCalculator:
|
|
335
|
+
"""Calculate sales tax, VAT, GST."""
|
|
336
|
+
|
|
337
|
+
def __init__(self):
|
|
338
|
+
# Tax rates by region
|
|
339
|
+
self.tax_rates = {
|
|
340
|
+
'US_CA': 0.0725, # California sales tax
|
|
341
|
+
'US_NY': 0.04, # New York sales tax
|
|
342
|
+
'GB': 0.20, # UK VAT
|
|
343
|
+
'DE': 0.19, # Germany VAT
|
|
344
|
+
'FR': 0.20, # France VAT
|
|
345
|
+
'AU': 0.10, # Australia GST
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
def calculate_tax(self, amount, customer):
|
|
349
|
+
"""Calculate applicable tax."""
|
|
350
|
+
# Determine tax jurisdiction
|
|
351
|
+
jurisdiction = self.get_tax_jurisdiction(customer)
|
|
352
|
+
|
|
353
|
+
if not jurisdiction:
|
|
354
|
+
return 0
|
|
355
|
+
|
|
356
|
+
# Get tax rate
|
|
357
|
+
tax_rate = self.tax_rates.get(jurisdiction, 0)
|
|
358
|
+
|
|
359
|
+
# Calculate tax
|
|
360
|
+
tax = amount * tax_rate
|
|
361
|
+
|
|
362
|
+
return {
|
|
363
|
+
'tax_amount': tax,
|
|
364
|
+
'tax_rate': tax_rate,
|
|
365
|
+
'jurisdiction': jurisdiction,
|
|
366
|
+
'tax_type': self.get_tax_type(jurisdiction)
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
def get_tax_jurisdiction(self, customer):
|
|
370
|
+
"""Determine tax jurisdiction based on customer location."""
|
|
371
|
+
if customer.country == 'US':
|
|
372
|
+
# US: Tax based on customer state
|
|
373
|
+
return f"US_{customer.state}"
|
|
374
|
+
elif customer.country in ['GB', 'DE', 'FR']:
|
|
375
|
+
# EU: VAT
|
|
376
|
+
return customer.country
|
|
377
|
+
elif customer.country == 'AU':
|
|
378
|
+
# Australia: GST
|
|
379
|
+
return 'AU'
|
|
380
|
+
else:
|
|
381
|
+
return None
|
|
382
|
+
|
|
383
|
+
def get_tax_type(self, jurisdiction):
|
|
384
|
+
"""Get type of tax for jurisdiction."""
|
|
385
|
+
if jurisdiction.startswith('US_'):
|
|
386
|
+
return 'Sales Tax'
|
|
387
|
+
elif jurisdiction in ['GB', 'DE', 'FR']:
|
|
388
|
+
return 'VAT'
|
|
389
|
+
elif jurisdiction == 'AU':
|
|
390
|
+
return 'GST'
|
|
391
|
+
return 'Tax'
|
|
392
|
+
|
|
393
|
+
def validate_vat_number(self, vat_number, country):
|
|
394
|
+
"""Validate EU VAT number."""
|
|
395
|
+
# Use VIES API for validation
|
|
396
|
+
# Returns True if valid, False otherwise
|
|
397
|
+
pass
|
|
398
|
+
```
|
|
399
|
+
|
|
400
|
+
## Invoice Generation
|
|
401
|
+
|
|
402
|
+
```python
|
|
403
|
+
class Invoice:
|
|
404
|
+
def __init__(self, customer_id, subscription_id=None):
|
|
405
|
+
self.id = generate_invoice_number()
|
|
406
|
+
self.customer_id = customer_id
|
|
407
|
+
self.subscription_id = subscription_id
|
|
408
|
+
self.status = 'draft'
|
|
409
|
+
self.line_items = []
|
|
410
|
+
self.subtotal = 0
|
|
411
|
+
self.tax = 0
|
|
412
|
+
self.total = 0
|
|
413
|
+
self.created_at = datetime.now()
|
|
414
|
+
|
|
415
|
+
def add_line_item(self, description, amount, quantity=1):
|
|
416
|
+
"""Add line item to invoice."""
|
|
417
|
+
line_item = {
|
|
418
|
+
'description': description,
|
|
419
|
+
'unit_amount': amount,
|
|
420
|
+
'quantity': quantity,
|
|
421
|
+
'total': amount * quantity
|
|
422
|
+
}
|
|
423
|
+
self.line_items.append(line_item)
|
|
424
|
+
self.subtotal += line_item['total']
|
|
425
|
+
|
|
426
|
+
def finalize(self):
|
|
427
|
+
"""Finalize invoice and calculate total."""
|
|
428
|
+
self.total = self.subtotal + self.tax
|
|
429
|
+
self.status = 'open'
|
|
430
|
+
self.finalized_at = datetime.now()
|
|
431
|
+
|
|
432
|
+
def mark_paid(self):
|
|
433
|
+
"""Mark invoice as paid."""
|
|
434
|
+
self.status = 'paid'
|
|
435
|
+
self.paid_at = datetime.now()
|
|
436
|
+
|
|
437
|
+
def to_pdf(self):
|
|
438
|
+
"""Generate PDF invoice."""
|
|
439
|
+
from reportlab.pdfgen import canvas
|
|
440
|
+
|
|
441
|
+
# Generate PDF
|
|
442
|
+
# Include: company info, customer info, line items, tax, total
|
|
443
|
+
pass
|
|
444
|
+
|
|
445
|
+
def to_html(self):
|
|
446
|
+
"""Generate HTML invoice."""
|
|
447
|
+
template = """
|
|
448
|
+
<!DOCTYPE html>
|
|
449
|
+
<html>
|
|
450
|
+
<head><title>Invoice #{invoice_number}</title></head>
|
|
451
|
+
<body>
|
|
452
|
+
<h1>Invoice #{invoice_number}</h1>
|
|
453
|
+
<p>Date: {date}</p>
|
|
454
|
+
<h2>Bill To:</h2>
|
|
455
|
+
<p>{customer_name}<br>{customer_address}</p>
|
|
456
|
+
<table>
|
|
457
|
+
<tr><th>Description</th><th>Quantity</th><th>Amount</th></tr>
|
|
458
|
+
{line_items}
|
|
459
|
+
</table>
|
|
460
|
+
<p>Subtotal: ${subtotal}</p>
|
|
461
|
+
<p>Tax: ${tax}</p>
|
|
462
|
+
<h3>Total: ${total}</h3>
|
|
463
|
+
</body>
|
|
464
|
+
</html>
|
|
465
|
+
"""
|
|
466
|
+
|
|
467
|
+
return template.format(
|
|
468
|
+
invoice_number=self.id,
|
|
469
|
+
date=self.created_at.strftime('%Y-%m-%d'),
|
|
470
|
+
customer_name=self.customer.name,
|
|
471
|
+
customer_address=self.customer.address,
|
|
472
|
+
line_items=self.render_line_items(),
|
|
473
|
+
subtotal=self.subtotal,
|
|
474
|
+
tax=self.tax,
|
|
475
|
+
total=self.total
|
|
476
|
+
)
|
|
477
|
+
```
|
|
478
|
+
|
|
479
|
+
## Usage-Based Billing
|
|
480
|
+
|
|
481
|
+
```python
|
|
482
|
+
class UsageBillingEngine:
|
|
483
|
+
"""Track and bill for usage."""
|
|
484
|
+
|
|
485
|
+
def track_usage(self, customer_id, metric, quantity):
|
|
486
|
+
"""Track usage event."""
|
|
487
|
+
UsageRecord.create(
|
|
488
|
+
customer_id=customer_id,
|
|
489
|
+
metric=metric,
|
|
490
|
+
quantity=quantity,
|
|
491
|
+
timestamp=datetime.now()
|
|
492
|
+
)
|
|
493
|
+
|
|
494
|
+
def calculate_usage_charges(self, subscription, period_start, period_end):
|
|
495
|
+
"""Calculate charges for usage in billing period."""
|
|
496
|
+
usage_records = UsageRecord.get_for_period(
|
|
497
|
+
subscription.customer_id,
|
|
498
|
+
period_start,
|
|
499
|
+
period_end
|
|
500
|
+
)
|
|
501
|
+
|
|
502
|
+
total_usage = sum(record.quantity for record in usage_records)
|
|
503
|
+
|
|
504
|
+
# Tiered pricing
|
|
505
|
+
if subscription.plan.pricing_model == 'tiered':
|
|
506
|
+
charge = self.calculate_tiered_pricing(total_usage, subscription.plan.tiers)
|
|
507
|
+
# Per-unit pricing
|
|
508
|
+
elif subscription.plan.pricing_model == 'per_unit':
|
|
509
|
+
charge = total_usage * subscription.plan.unit_price
|
|
510
|
+
# Volume pricing
|
|
511
|
+
elif subscription.plan.pricing_model == 'volume':
|
|
512
|
+
charge = self.calculate_volume_pricing(total_usage, subscription.plan.tiers)
|
|
513
|
+
|
|
514
|
+
return charge
|
|
515
|
+
|
|
516
|
+
def calculate_tiered_pricing(self, total_usage, tiers):
|
|
517
|
+
"""Calculate cost using tiered pricing."""
|
|
518
|
+
charge = 0
|
|
519
|
+
remaining = total_usage
|
|
520
|
+
|
|
521
|
+
for tier in sorted(tiers, key=lambda x: x['up_to']):
|
|
522
|
+
tier_usage = min(remaining, tier['up_to'] - tier['from'])
|
|
523
|
+
charge += tier_usage * tier['unit_price']
|
|
524
|
+
remaining -= tier_usage
|
|
525
|
+
|
|
526
|
+
if remaining <= 0:
|
|
527
|
+
break
|
|
528
|
+
|
|
529
|
+
return charge
|
|
530
|
+
```
|
|
531
|
+
|
|
532
|
+
## Resources
|
|
533
|
+
|
|
534
|
+
- **references/billing-cycles.md**: Billing cycle management
|
|
535
|
+
- **references/dunning-management.md**: Failed payment recovery
|
|
536
|
+
- **references/proration.md**: Prorated charge calculations
|
|
537
|
+
- **references/tax-calculation.md**: Tax/VAT/GST handling
|
|
538
|
+
- **references/invoice-lifecycle.md**: Invoice state management
|
|
539
|
+
- **assets/billing-state-machine.yaml**: Billing workflow
|
|
540
|
+
- **assets/invoice-template.html**: Invoice templates
|
|
541
|
+
- **assets/dunning-policy.yaml**: Dunning configuration
|
|
542
|
+
|
|
543
|
+
## Best Practices
|
|
544
|
+
|
|
545
|
+
1. **Automate Everything**: Minimize manual intervention
|
|
546
|
+
2. **Clear Communication**: Notify customers of billing events
|
|
547
|
+
3. **Flexible Retry Logic**: Balance recovery with customer experience
|
|
548
|
+
4. **Accurate Proration**: Fair calculation for plan changes
|
|
549
|
+
5. **Tax Compliance**: Calculate correct tax for jurisdiction
|
|
550
|
+
6. **Audit Trail**: Log all billing events
|
|
551
|
+
7. **Graceful Degradation**: Handle edge cases without breaking
|
|
552
|
+
|
|
553
|
+
## Common Pitfalls
|
|
554
|
+
|
|
555
|
+
- **Incorrect Proration**: Not accounting for partial periods
|
|
556
|
+
- **Missing Tax**: Forgetting to add tax to invoices
|
|
557
|
+
- **Aggressive Dunning**: Canceling too quickly
|
|
558
|
+
- **No Notifications**: Not informing customers of failures
|
|
559
|
+
- **Hardcoded Cycles**: Not supporting custom billing dates
|