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.
Files changed (79) hide show
  1. wagtail_subscriptions/__init__.py +8 -0
  2. wagtail_subscriptions/admin/__init__.py +1 -0
  3. wagtail_subscriptions/admin/features.py +40 -0
  4. wagtail_subscriptions/admin/subscription.py +39 -0
  5. wagtail_subscriptions/admin.py +104 -0
  6. wagtail_subscriptions/analytics.py +81 -0
  7. wagtail_subscriptions/apps.py +12 -0
  8. wagtail_subscriptions/context_processors.py +10 -0
  9. wagtail_subscriptions/forms.py +76 -0
  10. wagtail_subscriptions/management/__init__.py +1 -0
  11. wagtail_subscriptions/management/commands/__init__.py +1 -0
  12. wagtail_subscriptions/management/commands/check_expired_subscriptions.py +41 -0
  13. wagtail_subscriptions/management/commands/create_sample_plans.py +247 -0
  14. wagtail_subscriptions/management/commands/setup_subscription_permissions.py +73 -0
  15. wagtail_subscriptions/management/commands/subscription_maintenance.py +95 -0
  16. wagtail_subscriptions/management/commands/sync_subscription_plans.py +43 -0
  17. wagtail_subscriptions/management/commands/sync_tenant_plans.py +102 -0
  18. wagtail_subscriptions/migrations/0001_initial.py +347 -0
  19. wagtail_subscriptions/migrations/0002_invoice_payment_paymentmethod_subscriptiongroup_and_more.py +476 -0
  20. wagtail_subscriptions/migrations/__init__.py +1 -0
  21. wagtail_subscriptions/models/__init__.py +22 -0
  22. wagtail_subscriptions/models/audit.py +94 -0
  23. wagtail_subscriptions/models/core.py +167 -0
  24. wagtail_subscriptions/models/features.py +188 -0
  25. wagtail_subscriptions/models/payments.py +175 -0
  26. wagtail_subscriptions/models/permissions.py +39 -0
  27. wagtail_subscriptions/payments/__init__.py +12 -0
  28. wagtail_subscriptions/payments/base.py +120 -0
  29. wagtail_subscriptions/payments/paddle.py +210 -0
  30. wagtail_subscriptions/payments/paypal.py +169 -0
  31. wagtail_subscriptions/payments/stripe.py +155 -0
  32. wagtail_subscriptions/permissions/__init__.py +0 -0
  33. wagtail_subscriptions/permissions/decorators.py +50 -0
  34. wagtail_subscriptions/permissions/middleware.py +21 -0
  35. wagtail_subscriptions/permissions/mixins.py +56 -0
  36. wagtail_subscriptions/permissions/tenant_manager.py +82 -0
  37. wagtail_subscriptions/services/__init__.py +1 -0
  38. wagtail_subscriptions/services/invoice_service.py +90 -0
  39. wagtail_subscriptions/services/notification_service.py +132 -0
  40. wagtail_subscriptions/settings.py +28 -0
  41. wagtail_subscriptions/signals.py +26 -0
  42. wagtail_subscriptions/templates/wagtail_subscriptions/admin/analytics.html +85 -0
  43. wagtail_subscriptions/templates/wagtail_subscriptions/admin/customers.html +151 -0
  44. wagtail_subscriptions/templates/wagtail_subscriptions/admin/dashboard.html +399 -0
  45. wagtail_subscriptions/templates/wagtail_subscriptions/admin/features.html +103 -0
  46. wagtail_subscriptions/templates/wagtail_subscriptions/admin/plan_features.html +190 -0
  47. wagtail_subscriptions/templates/wagtail_subscriptions/admin/plans.html +72 -0
  48. wagtail_subscriptions/templates/wagtail_subscriptions/admin/settings.html +337 -0
  49. wagtail_subscriptions/templates/wagtail_subscriptions/api/subscription_detail.html +20 -0
  50. wagtail_subscriptions/templates/wagtail_subscriptions/base.html +54 -0
  51. wagtail_subscriptions/templates/wagtail_subscriptions/components/price_table.html +86 -0
  52. wagtail_subscriptions/templates/wagtail_subscriptions/components/pricing_cards.html +328 -0
  53. wagtail_subscriptions/templates/wagtail_subscriptions/pricing/pricing_page.html +116 -0
  54. wagtail_subscriptions/templates/wagtail_subscriptions/subscription/cancel.html +17 -0
  55. wagtail_subscriptions/templates/wagtail_subscriptions/subscription/portal.html +178 -0
  56. wagtail_subscriptions/templates/wagtail_subscriptions/subscription/subscribe.html +114 -0
  57. wagtail_subscriptions/templates/wagtail_subscriptions/subscription/success.html +101 -0
  58. wagtail_subscriptions/templates/wagtail_subscriptions/subscription/upgrade.html +26 -0
  59. wagtail_subscriptions/templates/wagtail_subscriptions/tags/subscription_status.html +19 -0
  60. wagtail_subscriptions/templatetags/__init__.py +1 -0
  61. wagtail_subscriptions/templatetags/subscription_tags.py +65 -0
  62. wagtail_subscriptions/urls.py +73 -0
  63. wagtail_subscriptions/urls_admin.py +21 -0
  64. wagtail_subscriptions/utils.py +103 -0
  65. wagtail_subscriptions/views/__init__.py +10 -0
  66. wagtail_subscriptions/views/admin.py +276 -0
  67. wagtail_subscriptions/views/analytics.py +26 -0
  68. wagtail_subscriptions/views/api.py +75 -0
  69. wagtail_subscriptions/views/plan_features.py +69 -0
  70. wagtail_subscriptions/views/plan_management.py +203 -0
  71. wagtail_subscriptions/views/pricing.py +19 -0
  72. wagtail_subscriptions/views/subscription.py +153 -0
  73. wagtail_subscriptions/views/webhooks.py +234 -0
  74. wagtail_subscriptions/wagtail_hooks.py +65 -0
  75. wagtail_subscriptions-0.1.0.dist-info/METADATA +316 -0
  76. wagtail_subscriptions-0.1.0.dist-info/RECORD +79 -0
  77. wagtail_subscriptions-0.1.0.dist-info/WHEEL +5 -0
  78. wagtail_subscriptions-0.1.0.dist-info/licenses/LICENSE +21 -0
  79. wagtail_subscriptions-0.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,8 @@
1
+ """
2
+ Wagtail Subscriptions - Comprehensive subscription management for Wagtail CMS
3
+ """
4
+
5
+ __version__ = "1.0.0"
6
+ __author__ = "Wagtail Subscriptions Team"
7
+
8
+ default_app_config = "wagtail_subscriptions.apps.WagtailSubscriptionsConfig"
@@ -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"))