wagtail-subscriptions 0.1.0__py3-none-any.whl
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.
- wagtail_subscriptions/__init__.py +8 -0
- wagtail_subscriptions/admin/__init__.py +1 -0
- wagtail_subscriptions/admin/features.py +40 -0
- wagtail_subscriptions/admin/subscription.py +39 -0
- wagtail_subscriptions/admin.py +104 -0
- wagtail_subscriptions/analytics.py +81 -0
- wagtail_subscriptions/apps.py +12 -0
- wagtail_subscriptions/context_processors.py +10 -0
- wagtail_subscriptions/forms.py +76 -0
- wagtail_subscriptions/management/__init__.py +1 -0
- wagtail_subscriptions/management/commands/__init__.py +1 -0
- wagtail_subscriptions/management/commands/check_expired_subscriptions.py +41 -0
- wagtail_subscriptions/management/commands/create_sample_plans.py +247 -0
- wagtail_subscriptions/management/commands/setup_subscription_permissions.py +73 -0
- wagtail_subscriptions/management/commands/subscription_maintenance.py +95 -0
- wagtail_subscriptions/management/commands/sync_subscription_plans.py +43 -0
- wagtail_subscriptions/management/commands/sync_tenant_plans.py +102 -0
- wagtail_subscriptions/migrations/0001_initial.py +347 -0
- wagtail_subscriptions/migrations/0002_invoice_payment_paymentmethod_subscriptiongroup_and_more.py +476 -0
- wagtail_subscriptions/migrations/__init__.py +1 -0
- wagtail_subscriptions/models/__init__.py +22 -0
- wagtail_subscriptions/models/audit.py +94 -0
- wagtail_subscriptions/models/core.py +167 -0
- wagtail_subscriptions/models/features.py +188 -0
- wagtail_subscriptions/models/payments.py +175 -0
- wagtail_subscriptions/models/permissions.py +39 -0
- wagtail_subscriptions/payments/__init__.py +12 -0
- wagtail_subscriptions/payments/base.py +120 -0
- wagtail_subscriptions/payments/paddle.py +210 -0
- wagtail_subscriptions/payments/paypal.py +169 -0
- wagtail_subscriptions/payments/stripe.py +155 -0
- wagtail_subscriptions/permissions/__init__.py +0 -0
- wagtail_subscriptions/permissions/decorators.py +50 -0
- wagtail_subscriptions/permissions/middleware.py +21 -0
- wagtail_subscriptions/permissions/mixins.py +56 -0
- wagtail_subscriptions/permissions/tenant_manager.py +82 -0
- wagtail_subscriptions/services/__init__.py +1 -0
- wagtail_subscriptions/services/invoice_service.py +90 -0
- wagtail_subscriptions/services/notification_service.py +132 -0
- wagtail_subscriptions/settings.py +28 -0
- wagtail_subscriptions/signals.py +26 -0
- wagtail_subscriptions/templates/wagtail_subscriptions/admin/analytics.html +85 -0
- wagtail_subscriptions/templates/wagtail_subscriptions/admin/customers.html +151 -0
- wagtail_subscriptions/templates/wagtail_subscriptions/admin/dashboard.html +399 -0
- wagtail_subscriptions/templates/wagtail_subscriptions/admin/features.html +103 -0
- wagtail_subscriptions/templates/wagtail_subscriptions/admin/plan_features.html +190 -0
- wagtail_subscriptions/templates/wagtail_subscriptions/admin/plans.html +72 -0
- wagtail_subscriptions/templates/wagtail_subscriptions/admin/settings.html +337 -0
- wagtail_subscriptions/templates/wagtail_subscriptions/api/subscription_detail.html +20 -0
- wagtail_subscriptions/templates/wagtail_subscriptions/base.html +54 -0
- wagtail_subscriptions/templates/wagtail_subscriptions/components/price_table.html +86 -0
- wagtail_subscriptions/templates/wagtail_subscriptions/components/pricing_cards.html +328 -0
- wagtail_subscriptions/templates/wagtail_subscriptions/pricing/pricing_page.html +116 -0
- wagtail_subscriptions/templates/wagtail_subscriptions/subscription/cancel.html +17 -0
- wagtail_subscriptions/templates/wagtail_subscriptions/subscription/portal.html +178 -0
- wagtail_subscriptions/templates/wagtail_subscriptions/subscription/subscribe.html +114 -0
- wagtail_subscriptions/templates/wagtail_subscriptions/subscription/success.html +101 -0
- wagtail_subscriptions/templates/wagtail_subscriptions/subscription/upgrade.html +26 -0
- wagtail_subscriptions/templates/wagtail_subscriptions/tags/subscription_status.html +19 -0
- wagtail_subscriptions/templatetags/__init__.py +1 -0
- wagtail_subscriptions/templatetags/subscription_tags.py +65 -0
- wagtail_subscriptions/urls.py +73 -0
- wagtail_subscriptions/urls_admin.py +21 -0
- wagtail_subscriptions/utils.py +103 -0
- wagtail_subscriptions/views/__init__.py +10 -0
- wagtail_subscriptions/views/admin.py +276 -0
- wagtail_subscriptions/views/analytics.py +26 -0
- wagtail_subscriptions/views/api.py +75 -0
- wagtail_subscriptions/views/plan_features.py +69 -0
- wagtail_subscriptions/views/plan_management.py +203 -0
- wagtail_subscriptions/views/pricing.py +19 -0
- wagtail_subscriptions/views/subscription.py +153 -0
- wagtail_subscriptions/views/webhooks.py +234 -0
- wagtail_subscriptions/wagtail_hooks.py +65 -0
- wagtail_subscriptions-0.1.0.dist-info/METADATA +316 -0
- wagtail_subscriptions-0.1.0.dist-info/RECORD +79 -0
- wagtail_subscriptions-0.1.0.dist-info/WHEEL +5 -0
- wagtail_subscriptions-0.1.0.dist-info/licenses/LICENSE +21 -0
- wagtail_subscriptions-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# Admin interfaces will be automatically discovered
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
from django.contrib import admin
|
|
2
|
+
|
|
3
|
+
from ..models import Feature, Module, PlanFeature, UsageRecord
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
@admin.register(Module)
|
|
7
|
+
class ModuleAdmin(admin.ModelAdmin):
|
|
8
|
+
list_display = ["name", "slug", "is_active", "sort_order"]
|
|
9
|
+
list_filter = ["is_active"]
|
|
10
|
+
search_fields = ["name", "slug"]
|
|
11
|
+
prepopulated_fields = {"slug": ("name",)}
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@admin.register(Feature)
|
|
15
|
+
class FeatureAdmin(admin.ModelAdmin):
|
|
16
|
+
list_display = ["name", "module", "feature_type", "is_active", "sort_order"]
|
|
17
|
+
list_filter = ["module", "feature_type", "is_active"]
|
|
18
|
+
search_fields = ["name", "slug"]
|
|
19
|
+
prepopulated_fields = {"slug": ("name",)}
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@admin.register(PlanFeature)
|
|
23
|
+
class PlanFeatureAdmin(admin.ModelAdmin):
|
|
24
|
+
list_display = ["plan", "feature", "is_included", "quota_override"]
|
|
25
|
+
list_filter = ["is_included", "plan", "feature__module"]
|
|
26
|
+
search_fields = ["plan__name", "feature__name"]
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@admin.register(UsageRecord)
|
|
30
|
+
class UsageRecordAdmin(admin.ModelAdmin):
|
|
31
|
+
list_display = [
|
|
32
|
+
"subscription",
|
|
33
|
+
"feature",
|
|
34
|
+
"usage_count",
|
|
35
|
+
"period_start",
|
|
36
|
+
"period_end",
|
|
37
|
+
]
|
|
38
|
+
list_filter = ["feature", "period_start"]
|
|
39
|
+
search_fields = ["subscription__user__email", "feature__name"]
|
|
40
|
+
readonly_fields = ["created_at", "updated_at"]
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
from django.contrib import admin
|
|
2
|
+
|
|
3
|
+
from ..models import Customer, Invoice, Payment, Subscription
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
@admin.register(Subscription)
|
|
7
|
+
class SubscriptionAdmin(admin.ModelAdmin):
|
|
8
|
+
list_display = ["user", "plan", "status", "current_period_end", "is_active"]
|
|
9
|
+
list_filter = ["status", "plan", "created_at"]
|
|
10
|
+
search_fields = ["user__username", "user__email", "external_id"]
|
|
11
|
+
readonly_fields = ["external_id", "created_at", "updated_at"]
|
|
12
|
+
|
|
13
|
+
def is_active(self, obj):
|
|
14
|
+
return obj.is_active
|
|
15
|
+
|
|
16
|
+
is_active.boolean = True
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@admin.register(Customer)
|
|
20
|
+
class CustomerAdmin(admin.ModelAdmin):
|
|
21
|
+
list_display = ["user", "company_name", "billing_email", "country"]
|
|
22
|
+
search_fields = ["user__username", "user__email", "company_name"]
|
|
23
|
+
readonly_fields = ["stripe_customer_id", "created_at", "updated_at"]
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@admin.register(Invoice)
|
|
27
|
+
class InvoiceAdmin(admin.ModelAdmin):
|
|
28
|
+
list_display = ["invoice_number", "customer", "status", "total", "due_date"]
|
|
29
|
+
list_filter = ["status", "created_at"]
|
|
30
|
+
search_fields = ["invoice_number", "customer__user__email"]
|
|
31
|
+
readonly_fields = ["external_id", "created_at", "updated_at"]
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@admin.register(Payment)
|
|
35
|
+
class PaymentAdmin(admin.ModelAdmin):
|
|
36
|
+
list_display = ["external_id", "customer", "amount", "status", "processed_at"]
|
|
37
|
+
list_filter = ["status", "payment_processor", "created_at"]
|
|
38
|
+
search_fields = ["external_id", "customer__user__email"]
|
|
39
|
+
readonly_fields = ["external_id", "created_at", "updated_at"]
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
from django.contrib import admin
|
|
2
|
+
from wagtail.contrib.modeladmin.options import ModelAdmin
|
|
3
|
+
from wagtail.snippets.models import register_snippet
|
|
4
|
+
|
|
5
|
+
from .models import (
|
|
6
|
+
Customer,
|
|
7
|
+
Feature,
|
|
8
|
+
Module,
|
|
9
|
+
PlanFeature,
|
|
10
|
+
Subscription,
|
|
11
|
+
SubscriptionPlan,
|
|
12
|
+
UsageRecord,
|
|
13
|
+
)
|
|
14
|
+
from .models.permissions import SubscriptionGroup, SubscriptionPermission
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@register_snippet
|
|
18
|
+
class SubscriptionPlanAdmin(ModelAdmin):
|
|
19
|
+
model = SubscriptionPlan
|
|
20
|
+
menu_label = "Subscription Plans"
|
|
21
|
+
menu_icon = "list-ul"
|
|
22
|
+
list_display = ["name", "price", "billing_period", "is_active", "trial_period_days"]
|
|
23
|
+
list_filter = ["billing_period", "is_active"]
|
|
24
|
+
search_fields = ["name", "description"]
|
|
25
|
+
ordering = ["sort_order", "name"]
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@register_snippet
|
|
29
|
+
class ModuleAdmin(ModelAdmin):
|
|
30
|
+
model = Module
|
|
31
|
+
menu_label = "Modules"
|
|
32
|
+
menu_icon = "cogs"
|
|
33
|
+
list_display = ["name", "slug", "is_active", "sort_order"]
|
|
34
|
+
list_filter = ["is_active"]
|
|
35
|
+
search_fields = ["name", "description"]
|
|
36
|
+
ordering = ["sort_order", "name"]
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@register_snippet
|
|
40
|
+
class FeatureAdmin(ModelAdmin):
|
|
41
|
+
model = Feature
|
|
42
|
+
menu_label = "Features"
|
|
43
|
+
menu_icon = "cog"
|
|
44
|
+
list_display = ["name", "module", "feature_type", "is_active"]
|
|
45
|
+
list_filter = ["module", "feature_type", "is_active"]
|
|
46
|
+
search_fields = ["name", "description"]
|
|
47
|
+
ordering = ["module", "sort_order", "name"]
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
@admin.register(PlanFeature)
|
|
51
|
+
class PlanFeatureAdmin(admin.ModelAdmin):
|
|
52
|
+
list_display = ["plan", "feature", "is_included", "quota_override"]
|
|
53
|
+
list_filter = ["is_included", "plan", "feature__module"]
|
|
54
|
+
search_fields = ["plan__name", "feature__name"]
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
@admin.register(Subscription)
|
|
58
|
+
class SubscriptionAdmin(admin.ModelAdmin):
|
|
59
|
+
list_display = [
|
|
60
|
+
"user",
|
|
61
|
+
"plan",
|
|
62
|
+
"status",
|
|
63
|
+
"current_period_start",
|
|
64
|
+
"current_period_end",
|
|
65
|
+
]
|
|
66
|
+
list_filter = ["status", "plan", "payment_processor"]
|
|
67
|
+
search_fields = ["user__username", "user__email", "plan__name"]
|
|
68
|
+
readonly_fields = ["external_id", "created_at", "updated_at"]
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
@admin.register(Customer)
|
|
72
|
+
class CustomerAdmin(admin.ModelAdmin):
|
|
73
|
+
list_display = ["user", "company_name", "billing_email", "created_at"]
|
|
74
|
+
search_fields = ["user__username", "user__email", "company_name", "billing_email"]
|
|
75
|
+
readonly_fields = ["stripe_customer_id", "created_at", "updated_at"]
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
@admin.register(UsageRecord)
|
|
79
|
+
class UsageRecordAdmin(admin.ModelAdmin):
|
|
80
|
+
list_display = [
|
|
81
|
+
"subscription",
|
|
82
|
+
"feature",
|
|
83
|
+
"usage_count",
|
|
84
|
+
"period_start",
|
|
85
|
+
"period_end",
|
|
86
|
+
]
|
|
87
|
+
list_filter = ["feature", "subscription__plan"]
|
|
88
|
+
search_fields = ["subscription__user__username", "feature__name"]
|
|
89
|
+
readonly_fields = ["created_at", "updated_at"]
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
@admin.register(SubscriptionPermission)
|
|
93
|
+
class SubscriptionPermissionAdmin(admin.ModelAdmin):
|
|
94
|
+
list_display = ["feature", "permission", "created_at"]
|
|
95
|
+
list_filter = ["feature__module", "permission__content_type"]
|
|
96
|
+
search_fields = ["feature__name", "permission__name"]
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
@admin.register(SubscriptionGroup)
|
|
100
|
+
class SubscriptionGroupAdmin(admin.ModelAdmin):
|
|
101
|
+
list_display = ["plan", "name", "created_at"]
|
|
102
|
+
list_filter = ["plan"]
|
|
103
|
+
search_fields = ["plan__name", "name"]
|
|
104
|
+
filter_horizontal = ["permissions"]
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
from datetime import timedelta
|
|
2
|
+
from decimal import Decimal
|
|
3
|
+
|
|
4
|
+
from django.db.models import Avg, Count, Sum
|
|
5
|
+
from django.utils import timezone
|
|
6
|
+
|
|
7
|
+
from .models import Subscription, UsageRecord
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class SubscriptionAnalytics:
|
|
11
|
+
"""Analytics for subscription metrics"""
|
|
12
|
+
|
|
13
|
+
@staticmethod
|
|
14
|
+
def get_mrr():
|
|
15
|
+
"""Calculate Monthly Recurring Revenue"""
|
|
16
|
+
active_subs = Subscription.objects.filter(status__in=["active", "trialing"])
|
|
17
|
+
monthly_revenue = Decimal("0.00")
|
|
18
|
+
|
|
19
|
+
for sub in active_subs:
|
|
20
|
+
if sub.plan.billing_period == "monthly":
|
|
21
|
+
monthly_revenue += sub.plan.price
|
|
22
|
+
elif sub.plan.billing_period == "yearly":
|
|
23
|
+
monthly_revenue += sub.plan.price / 12
|
|
24
|
+
elif sub.plan.billing_period == "quarterly":
|
|
25
|
+
monthly_revenue += sub.plan.price / 3
|
|
26
|
+
|
|
27
|
+
return monthly_revenue
|
|
28
|
+
|
|
29
|
+
@staticmethod
|
|
30
|
+
def get_churn_rate(days=30):
|
|
31
|
+
"""Calculate churn rate for the last N days"""
|
|
32
|
+
end_date = timezone.now()
|
|
33
|
+
start_date = end_date - timedelta(days=days)
|
|
34
|
+
|
|
35
|
+
total_start = Subscription.objects.filter(created_at__lt=start_date).count()
|
|
36
|
+
churned = Subscription.objects.filter(
|
|
37
|
+
canceled_at__gte=start_date, canceled_at__lt=end_date
|
|
38
|
+
).count()
|
|
39
|
+
|
|
40
|
+
return (churned / total_start * 100) if total_start > 0 else 0
|
|
41
|
+
|
|
42
|
+
@staticmethod
|
|
43
|
+
def get_conversion_rate(days=30):
|
|
44
|
+
"""Calculate trial to paid conversion rate"""
|
|
45
|
+
end_date = timezone.now()
|
|
46
|
+
start_date = end_date - timedelta(days=days)
|
|
47
|
+
|
|
48
|
+
trials = Subscription.objects.filter(
|
|
49
|
+
created_at__gte=start_date, created_at__lt=end_date, trial_end__isnull=False
|
|
50
|
+
).count()
|
|
51
|
+
|
|
52
|
+
conversions = Subscription.objects.filter(
|
|
53
|
+
created_at__gte=start_date,
|
|
54
|
+
created_at__lt=end_date,
|
|
55
|
+
trial_end__isnull=False,
|
|
56
|
+
status="active",
|
|
57
|
+
).count()
|
|
58
|
+
|
|
59
|
+
return (conversions / trials * 100) if trials > 0 else 0
|
|
60
|
+
|
|
61
|
+
@staticmethod
|
|
62
|
+
def get_feature_usage(feature_slug, days=30):
|
|
63
|
+
"""Get feature usage statistics"""
|
|
64
|
+
from .models import Feature
|
|
65
|
+
|
|
66
|
+
# Validate feature exists to prevent injection
|
|
67
|
+
try:
|
|
68
|
+
feature = Feature.objects.get(slug=feature_slug)
|
|
69
|
+
except Feature.DoesNotExist:
|
|
70
|
+
return {"total_usage": 0, "avg_usage": 0, "unique_users": 0}
|
|
71
|
+
|
|
72
|
+
end_date = timezone.now()
|
|
73
|
+
start_date = end_date - timedelta(days=days)
|
|
74
|
+
|
|
75
|
+
usage = UsageRecord.objects.filter(feature=feature, period_start__gte=start_date).aggregate(
|
|
76
|
+
total_usage=Sum("usage_count"),
|
|
77
|
+
avg_usage=Avg("usage_count"),
|
|
78
|
+
unique_users=Count("subscription__user", distinct=True),
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
return usage
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
from django.apps import AppConfig
|
|
2
|
+
from django.utils.translation import gettext_lazy as _
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class WagtailSubscriptionsConfig(AppConfig):
|
|
6
|
+
default_auto_field = "django.db.models.BigAutoField"
|
|
7
|
+
name = "wagtail_subscriptions"
|
|
8
|
+
verbose_name = _("Wagtail Subscriptions")
|
|
9
|
+
|
|
10
|
+
def ready(self):
|
|
11
|
+
# Import signal handlers
|
|
12
|
+
from . import signals # noqa
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
from .permissions.tenant_manager import TenantSubscriptionManager
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def subscription_context(request):
|
|
5
|
+
"""Add subscription context to all templates"""
|
|
6
|
+
return {
|
|
7
|
+
"subscription_info": TenantSubscriptionManager.get_subscriber_info(request),
|
|
8
|
+
"is_multi_tenant": TenantSubscriptionManager.is_multi_tenant(),
|
|
9
|
+
"current_plan": TenantSubscriptionManager.get_active_plan(request),
|
|
10
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
from django import forms
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class PaymentProcessorConfigForm(forms.Form):
|
|
5
|
+
processor = forms.ChoiceField(
|
|
6
|
+
choices=[
|
|
7
|
+
("stripe", "Stripe"),
|
|
8
|
+
("paddle", "Paddle"),
|
|
9
|
+
("paypal", "PayPal"),
|
|
10
|
+
],
|
|
11
|
+
widget=forms.Select(attrs={"class": "form-control"}),
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
# Stripe fields
|
|
15
|
+
stripe_public_key = forms.CharField(
|
|
16
|
+
required=False,
|
|
17
|
+
widget=forms.TextInput(attrs={"class": "form-control", "placeholder": "pk_test_..."}),
|
|
18
|
+
)
|
|
19
|
+
stripe_secret_key = forms.CharField(
|
|
20
|
+
required=False,
|
|
21
|
+
widget=forms.PasswordInput(attrs={"class": "form-control", "placeholder": "sk_test_..."}),
|
|
22
|
+
)
|
|
23
|
+
stripe_webhook_secret = forms.CharField(
|
|
24
|
+
required=False,
|
|
25
|
+
widget=forms.PasswordInput(attrs={"class": "form-control", "placeholder": "whsec_..."}),
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
# Paddle fields
|
|
29
|
+
paddle_vendor_id = forms.CharField(
|
|
30
|
+
required=False,
|
|
31
|
+
widget=forms.TextInput(attrs={"class": "form-control", "placeholder": "12345"}),
|
|
32
|
+
)
|
|
33
|
+
paddle_vendor_auth_code = forms.CharField(
|
|
34
|
+
required=False, widget=forms.PasswordInput(attrs={"class": "form-control"})
|
|
35
|
+
)
|
|
36
|
+
paddle_public_key = forms.CharField(
|
|
37
|
+
required=False,
|
|
38
|
+
widget=forms.Textarea(attrs={"class": "form-control", "rows": 4}),
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
# PayPal fields
|
|
42
|
+
paypal_client_id = forms.CharField(
|
|
43
|
+
required=False, widget=forms.TextInput(attrs={"class": "form-control"})
|
|
44
|
+
)
|
|
45
|
+
paypal_client_secret = forms.CharField(
|
|
46
|
+
required=False, widget=forms.PasswordInput(attrs={"class": "form-control"})
|
|
47
|
+
)
|
|
48
|
+
paypal_mode = forms.ChoiceField(
|
|
49
|
+
choices=[("sandbox", "Sandbox"), ("live", "Live")],
|
|
50
|
+
required=False,
|
|
51
|
+
widget=forms.Select(attrs={"class": "form-control"}),
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
def clean(self):
|
|
55
|
+
cleaned_data = super().clean()
|
|
56
|
+
processor = cleaned_data.get("processor")
|
|
57
|
+
|
|
58
|
+
if processor == "stripe":
|
|
59
|
+
if not cleaned_data.get("stripe_secret_key"):
|
|
60
|
+
raise forms.ValidationError("Stripe secret key is required for Stripe processor")
|
|
61
|
+
elif processor == "paypal":
|
|
62
|
+
if not cleaned_data.get("paypal_client_id") or not cleaned_data.get(
|
|
63
|
+
"paypal_client_secret"
|
|
64
|
+
):
|
|
65
|
+
raise forms.ValidationError(
|
|
66
|
+
"PayPal client ID and secret are required for PayPal processor"
|
|
67
|
+
)
|
|
68
|
+
elif processor == "paddle":
|
|
69
|
+
if not cleaned_data.get("paddle_vendor_id") or not cleaned_data.get(
|
|
70
|
+
"paddle_vendor_auth_code"
|
|
71
|
+
):
|
|
72
|
+
raise forms.ValidationError(
|
|
73
|
+
"Paddle vendor ID and auth code are required for Paddle processor"
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
return cleaned_data
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# Management commands package
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# Management commands
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
from django.core.management.base import BaseCommand
|
|
2
|
+
from django.utils import timezone
|
|
3
|
+
|
|
4
|
+
from wagtail_subscriptions.models import Subscription
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class Command(BaseCommand):
|
|
8
|
+
help = "Check for expired subscriptions and update their status"
|
|
9
|
+
|
|
10
|
+
def add_arguments(self, parser):
|
|
11
|
+
parser.add_argument(
|
|
12
|
+
"--dry-run",
|
|
13
|
+
action="store_true",
|
|
14
|
+
help="Show what would be updated without making changes",
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
def handle(self, *args, **options):
|
|
18
|
+
dry_run = options["dry_run"]
|
|
19
|
+
now = timezone.now()
|
|
20
|
+
|
|
21
|
+
# Find expired trials
|
|
22
|
+
expired_trials = Subscription.objects.filter(status="trialing", trial_end__lt=now)
|
|
23
|
+
|
|
24
|
+
# Find expired subscriptions
|
|
25
|
+
expired_subscriptions = Subscription.objects.filter(
|
|
26
|
+
status="active", current_period_end__lt=now
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
if dry_run:
|
|
30
|
+
self.stdout.write(f"Would update {expired_trials.count()} expired trials")
|
|
31
|
+
self.stdout.write(f"Would update {expired_subscriptions.count()} expired subscriptions")
|
|
32
|
+
else:
|
|
33
|
+
# Update expired trials
|
|
34
|
+
updated_trials = expired_trials.update(status="past_due")
|
|
35
|
+
self.stdout.write(f"Updated {updated_trials} expired trials")
|
|
36
|
+
|
|
37
|
+
# Update expired subscriptions
|
|
38
|
+
updated_subs = expired_subscriptions.update(status="past_due")
|
|
39
|
+
self.stdout.write(f"Updated {updated_subs} expired subscriptions")
|
|
40
|
+
|
|
41
|
+
self.stdout.write(self.style.SUCCESS("Successfully processed expired subscriptions"))
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
from django.core.management.base import BaseCommand
|
|
2
|
+
|
|
3
|
+
from wagtail_subscriptions.models import Feature, Module, PlanFeature, SubscriptionPlan
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class Command(BaseCommand):
|
|
7
|
+
help = "Create sample subscription plans and features"
|
|
8
|
+
|
|
9
|
+
def handle(self, *args, **options):
|
|
10
|
+
# Create modules based on cashsheet structure
|
|
11
|
+
accounting_module, _ = Module.objects.get_or_create(
|
|
12
|
+
slug="accounting",
|
|
13
|
+
defaults={
|
|
14
|
+
"name": "Accounting",
|
|
15
|
+
"description": "Core accounting and bookkeeping features",
|
|
16
|
+
"icon": "calculator",
|
|
17
|
+
"is_active": True,
|
|
18
|
+
"sort_order": 1,
|
|
19
|
+
},
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
reporting_module, _ = Module.objects.get_or_create(
|
|
23
|
+
slug="reporting",
|
|
24
|
+
defaults={
|
|
25
|
+
"name": "Reporting & Analytics",
|
|
26
|
+
"description": "Financial reports and business analytics",
|
|
27
|
+
"icon": "chart-bar",
|
|
28
|
+
"is_active": True,
|
|
29
|
+
"sort_order": 2,
|
|
30
|
+
},
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
collaboration_module, _ = Module.objects.get_or_create(
|
|
34
|
+
slug="collaboration",
|
|
35
|
+
defaults={
|
|
36
|
+
"name": "Team Collaboration",
|
|
37
|
+
"description": "Multi-user access and team features",
|
|
38
|
+
"icon": "users",
|
|
39
|
+
"is_active": True,
|
|
40
|
+
"sort_order": 3,
|
|
41
|
+
},
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
# Create features based on cashsheet functionality
|
|
45
|
+
features_data = [
|
|
46
|
+
# Accounting Module Features
|
|
47
|
+
{
|
|
48
|
+
"module": accounting_module,
|
|
49
|
+
"name": "Chart of Accounts",
|
|
50
|
+
"slug": "chart-of-accounts",
|
|
51
|
+
"feature_type": "binary",
|
|
52
|
+
"description": "Create and manage chart of accounts",
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
"module": accounting_module,
|
|
56
|
+
"name": "Journal Entries",
|
|
57
|
+
"slug": "journal-entries",
|
|
58
|
+
"feature_type": "quota",
|
|
59
|
+
"description": "Monthly journal entries limit",
|
|
60
|
+
"default_quota": 100,
|
|
61
|
+
"quota_unit": "entries/month",
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
"module": accounting_module,
|
|
65
|
+
"name": "Bank Reconciliation",
|
|
66
|
+
"slug": "bank-reconciliation",
|
|
67
|
+
"feature_type": "binary",
|
|
68
|
+
"description": "Automated bank reconciliation",
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
"module": accounting_module,
|
|
72
|
+
"name": "Invoicing",
|
|
73
|
+
"slug": "invoicing",
|
|
74
|
+
"feature_type": "quota",
|
|
75
|
+
"description": "Create and send invoices",
|
|
76
|
+
"default_quota": 50,
|
|
77
|
+
"quota_unit": "invoices/month",
|
|
78
|
+
},
|
|
79
|
+
# Reporting Module Features
|
|
80
|
+
{
|
|
81
|
+
"module": reporting_module,
|
|
82
|
+
"name": "Financial Statements",
|
|
83
|
+
"slug": "financial-statements",
|
|
84
|
+
"feature_type": "binary",
|
|
85
|
+
"description": "Balance Sheet, Income Statement, Cash Flow",
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
"module": reporting_module,
|
|
89
|
+
"name": "Custom Reports",
|
|
90
|
+
"slug": "custom-reports",
|
|
91
|
+
"feature_type": "quota",
|
|
92
|
+
"description": "Create custom financial reports",
|
|
93
|
+
"default_quota": 5,
|
|
94
|
+
"quota_unit": "reports",
|
|
95
|
+
},
|
|
96
|
+
{
|
|
97
|
+
"module": reporting_module,
|
|
98
|
+
"name": "Advanced Analytics",
|
|
99
|
+
"slug": "advanced-analytics",
|
|
100
|
+
"feature_type": "binary",
|
|
101
|
+
"description": "Business intelligence and forecasting",
|
|
102
|
+
},
|
|
103
|
+
# Collaboration Module Features
|
|
104
|
+
{
|
|
105
|
+
"module": collaboration_module,
|
|
106
|
+
"name": "Team Members",
|
|
107
|
+
"slug": "team-members",
|
|
108
|
+
"feature_type": "quota",
|
|
109
|
+
"description": "Number of team members",
|
|
110
|
+
"default_quota": 1,
|
|
111
|
+
"quota_unit": "users",
|
|
112
|
+
},
|
|
113
|
+
{
|
|
114
|
+
"module": collaboration_module,
|
|
115
|
+
"name": "Organizations",
|
|
116
|
+
"slug": "organizations",
|
|
117
|
+
"feature_type": "quota",
|
|
118
|
+
"description": "Number of organizations/entities",
|
|
119
|
+
"default_quota": 1,
|
|
120
|
+
"quota_unit": "organizations",
|
|
121
|
+
},
|
|
122
|
+
{
|
|
123
|
+
"module": collaboration_module,
|
|
124
|
+
"name": "Priority Support",
|
|
125
|
+
"slug": "priority-support",
|
|
126
|
+
"feature_type": "binary",
|
|
127
|
+
"description": "24/7 priority customer support",
|
|
128
|
+
},
|
|
129
|
+
]
|
|
130
|
+
|
|
131
|
+
features = {}
|
|
132
|
+
for feature_data in features_data:
|
|
133
|
+
feature, _ = Feature.objects.get_or_create(
|
|
134
|
+
slug=feature_data["slug"],
|
|
135
|
+
module=feature_data["module"],
|
|
136
|
+
defaults=feature_data,
|
|
137
|
+
)
|
|
138
|
+
features[feature_data["slug"]] = feature
|
|
139
|
+
|
|
140
|
+
# Create plans based on cashsheet pricing structure
|
|
141
|
+
plans_data = [
|
|
142
|
+
{
|
|
143
|
+
"name": "Starter",
|
|
144
|
+
"slug": "starter",
|
|
145
|
+
"price": 0.00,
|
|
146
|
+
"billing_period": "monthly",
|
|
147
|
+
"trial_period_days": 0,
|
|
148
|
+
"description": "Perfect for freelancers and small businesses getting started",
|
|
149
|
+
"sort_order": 1,
|
|
150
|
+
},
|
|
151
|
+
{
|
|
152
|
+
"name": "Professional",
|
|
153
|
+
"slug": "professional",
|
|
154
|
+
"price": 29.00,
|
|
155
|
+
"billing_period": "monthly",
|
|
156
|
+
"trial_period_days": 14,
|
|
157
|
+
"description": "For growing businesses with advanced accounting needs",
|
|
158
|
+
"sort_order": 2,
|
|
159
|
+
},
|
|
160
|
+
{
|
|
161
|
+
"name": "Premium",
|
|
162
|
+
"slug": "premium",
|
|
163
|
+
"price": 79.00,
|
|
164
|
+
"billing_period": "monthly",
|
|
165
|
+
"trial_period_days": 30,
|
|
166
|
+
"description": "For large organizations with unlimited access",
|
|
167
|
+
"sort_order": 3,
|
|
168
|
+
},
|
|
169
|
+
]
|
|
170
|
+
|
|
171
|
+
for plan_data in plans_data:
|
|
172
|
+
plan, created = SubscriptionPlan.objects.get_or_create(
|
|
173
|
+
slug=plan_data["slug"], defaults=plan_data
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
if created:
|
|
177
|
+
self.stdout.write(f"Created plan: {plan.name}")
|
|
178
|
+
|
|
179
|
+
# Associate features with plans based on cashsheet structure
|
|
180
|
+
if plan.slug == "starter":
|
|
181
|
+
# Starter plan - basic features with limits
|
|
182
|
+
starter_features = {
|
|
183
|
+
"chart-of-accounts": {"quota": None},
|
|
184
|
+
"journal-entries": {"quota": 50},
|
|
185
|
+
"invoicing": {"quota": 10},
|
|
186
|
+
"financial-statements": {"quota": None},
|
|
187
|
+
"team-members": {"quota": 1},
|
|
188
|
+
"organizations": {"quota": 1},
|
|
189
|
+
}
|
|
190
|
+
for feature_slug, config in starter_features.items():
|
|
191
|
+
PlanFeature.objects.get_or_create(
|
|
192
|
+
plan=plan,
|
|
193
|
+
feature=features[feature_slug],
|
|
194
|
+
defaults={
|
|
195
|
+
"is_included": True,
|
|
196
|
+
"quota_override": config["quota"],
|
|
197
|
+
},
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
elif plan.slug == "professional":
|
|
201
|
+
# Professional plan - expanded features
|
|
202
|
+
pro_features = {
|
|
203
|
+
"chart-of-accounts": {"quota": None},
|
|
204
|
+
"journal-entries": {"quota": 500},
|
|
205
|
+
"bank-reconciliation": {"quota": None},
|
|
206
|
+
"invoicing": {"quota": 100},
|
|
207
|
+
"financial-statements": {"quota": None},
|
|
208
|
+
"custom-reports": {"quota": 10},
|
|
209
|
+
"advanced-analytics": {"quota": None},
|
|
210
|
+
"team-members": {"quota": 5},
|
|
211
|
+
"organizations": {"quota": 3},
|
|
212
|
+
}
|
|
213
|
+
for feature_slug, config in pro_features.items():
|
|
214
|
+
PlanFeature.objects.get_or_create(
|
|
215
|
+
plan=plan,
|
|
216
|
+
feature=features[feature_slug],
|
|
217
|
+
defaults={
|
|
218
|
+
"is_included": True,
|
|
219
|
+
"quota_override": config["quota"],
|
|
220
|
+
},
|
|
221
|
+
)
|
|
222
|
+
|
|
223
|
+
elif plan.slug == "premium":
|
|
224
|
+
# Premium plan - unlimited access
|
|
225
|
+
premium_features = {
|
|
226
|
+
"chart-of-accounts": {"quota": None},
|
|
227
|
+
"journal-entries": {"quota": 9999},
|
|
228
|
+
"bank-reconciliation": {"quota": None},
|
|
229
|
+
"invoicing": {"quota": 9999},
|
|
230
|
+
"financial-statements": {"quota": None},
|
|
231
|
+
"custom-reports": {"quota": 9999},
|
|
232
|
+
"advanced-analytics": {"quota": None},
|
|
233
|
+
"team-members": {"quota": 9999},
|
|
234
|
+
"organizations": {"quota": 9999},
|
|
235
|
+
"priority-support": {"quota": None},
|
|
236
|
+
}
|
|
237
|
+
for feature_slug, config in premium_features.items():
|
|
238
|
+
PlanFeature.objects.get_or_create(
|
|
239
|
+
plan=plan,
|
|
240
|
+
feature=features[feature_slug],
|
|
241
|
+
defaults={
|
|
242
|
+
"is_included": True,
|
|
243
|
+
"quota_override": config["quota"],
|
|
244
|
+
},
|
|
245
|
+
)
|
|
246
|
+
|
|
247
|
+
self.stdout.write(self.style.SUCCESS("Successfully created sample plans and features"))
|