Complete Implemenation of tenancy

This commit is contained in:
IGNY8 VPS (Salman)
2025-12-09 00:11:35 +00:00
parent c54db6c2d9
commit bfbade7624
25 changed files with 4959 additions and 35 deletions

View File

@@ -0,0 +1,25 @@
# Generated by Django 5.2.8 on 2025-12-08 22:42
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('igny8_core_auth', '0009_add_plan_annual_discount_and_featured'),
]
operations = [
migrations.AddField(
model_name='subscription',
name='plan',
field=models.ForeignKey(blank=True, help_text='Subscription plan (tracks historical plan even if account changes plan)', null=True, on_delete=django.db.models.deletion.PROTECT, related_name='subscriptions', to='igny8_core_auth.plan'),
),
migrations.AlterField(
model_name='site',
name='industry',
field=models.ForeignKey(default=21, help_text='Industry this site belongs to (required for sector creation)', on_delete=django.db.models.deletion.PROTECT, related_name='sites', to='igny8_core_auth.industry'),
preserve_default=False,
),
]

View File

@@ -0,0 +1,17 @@
# Generated by Django 5.2.8 on 2025-12-08 22:52
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('igny8_core_auth', '0010_add_subscription_plan_and_require_site_industry'),
]
operations = [
migrations.RemoveField(
model_name='subscription',
name='payment_method',
),
]

View File

@@ -124,6 +124,21 @@ class Account(SoftDeletableModel):
def __str__(self):
return self.name
@property
def default_payment_method(self):
"""Get default payment method from AccountPaymentMethod table"""
try:
from igny8_core.business.billing.models import AccountPaymentMethod
method = AccountPaymentMethod.objects.filter(
account=self,
is_default=True,
is_enabled=True
).first()
return method.type if method else self.payment_method
except Exception:
# Fallback to field if table doesn't exist or error
return self.payment_method
def is_system_account(self):
"""Check if this account is a system account with highest access level."""
# System accounts bypass all filtering restrictions
@@ -230,6 +245,14 @@ class Subscription(models.Model):
]
account = models.OneToOneField('igny8_core_auth.Account', on_delete=models.CASCADE, related_name='subscription', db_column='tenant_id')
plan = models.ForeignKey(
'igny8_core_auth.Plan',
on_delete=models.PROTECT,
related_name='subscriptions',
null=True,
blank=True,
help_text='Subscription plan (tracks historical plan even if account changes plan)'
)
stripe_subscription_id = models.CharField(
max_length=255,
blank=True,
@@ -237,12 +260,6 @@ class Subscription(models.Model):
db_index=True,
help_text='Stripe subscription ID (when using Stripe)'
)
payment_method = models.CharField(
max_length=30,
choices=PAYMENT_METHOD_CHOICES,
default='stripe',
help_text='Payment method for this subscription'
)
external_payment_id = models.CharField(
max_length=255,
blank=True,
@@ -255,6 +272,14 @@ class Subscription(models.Model):
cancel_at_period_end = models.BooleanField(default=False)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
@property
def payment_method(self):
"""Get payment method from account's default payment method"""
if hasattr(self.account, 'default_payment_method'):
return self.account.default_payment_method
# Fallback to account.payment_method field if property doesn't exist yet
return getattr(self.account, 'payment_method', 'stripe')
class Meta:
db_table = 'igny8_subscriptions'
@@ -286,9 +311,7 @@ class Site(SoftDeletableModel, AccountBaseModel):
'igny8_core_auth.Industry',
on_delete=models.PROTECT,
related_name='sites',
null=True,
blank=True,
help_text="Industry this site belongs to"
help_text="Industry this site belongs to (required for sector creation)"
)
is_active = models.BooleanField(default=True, db_index=True)
status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='active')

View File

@@ -267,10 +267,19 @@ class RegisterSerializer(serializers.Serializer):
)
plan_slug = serializers.CharField(max_length=50, required=False)
payment_method = serializers.ChoiceField(
choices=['stripe', 'paypal', 'bank_transfer'],
choices=['stripe', 'paypal', 'bank_transfer', 'local_wallet'],
default='bank_transfer',
required=False
)
# Billing information fields
billing_email = serializers.EmailField(required=False, allow_blank=True)
billing_address_line1 = serializers.CharField(max_length=255, required=False, allow_blank=True)
billing_address_line2 = serializers.CharField(max_length=255, required=False, allow_blank=True)
billing_city = serializers.CharField(max_length=100, required=False, allow_blank=True)
billing_state = serializers.CharField(max_length=100, required=False, allow_blank=True)
billing_postal_code = serializers.CharField(max_length=20, required=False, allow_blank=True)
billing_country = serializers.CharField(max_length=2, required=False, allow_blank=True)
tax_id = serializers.CharField(max_length=100, required=False, allow_blank=True)
def validate(self, attrs):
if attrs['password'] != attrs['password_confirm']:
@@ -287,7 +296,7 @@ class RegisterSerializer(serializers.Serializer):
def create(self, validated_data):
from django.db import transaction
from igny8_core.business.billing.models import CreditTransaction
from igny8_core.business.billing.models import Subscription
from igny8_core.auth.models import Subscription
from igny8_core.business.billing.models import AccountPaymentMethod
from igny8_core.business.billing.services.invoice_service import InvoiceService
from django.utils import timezone
@@ -371,6 +380,15 @@ class RegisterSerializer(serializers.Serializer):
credits=initial_credits,
status=account_status,
payment_method=validated_data.get('payment_method') or 'bank_transfer',
# Save billing information
billing_email=validated_data.get('billing_email', '') or validated_data.get('email', ''),
billing_address_line1=validated_data.get('billing_address_line1', ''),
billing_address_line2=validated_data.get('billing_address_line2', ''),
billing_city=validated_data.get('billing_city', ''),
billing_state=validated_data.get('billing_state', ''),
billing_postal_code=validated_data.get('billing_postal_code', ''),
billing_country=validated_data.get('billing_country', ''),
tax_id=validated_data.get('tax_id', ''),
)
# Log initial credit transaction only for free/trial accounts with credits
@@ -392,13 +410,14 @@ class RegisterSerializer(serializers.Serializer):
user.account = account
user.save()
# For paid plans, create subscription, invoice, and default bank transfer method
# For paid plans, create subscription, invoice, and default payment method
if plan_slug and plan_slug in paid_plans:
payment_method = validated_data.get('payment_method', 'bank_transfer')
subscription = Subscription.objects.create(
account=account,
plan=plan,
status='pending_payment',
payment_method='bank_transfer',
external_payment_id=None,
current_period_start=billing_period_start,
current_period_end=billing_period_end,
@@ -410,15 +429,21 @@ class RegisterSerializer(serializers.Serializer):
billing_period_start=billing_period_start,
billing_period_end=billing_period_end,
)
# Seed a default bank transfer payment method for the account
# Create AccountPaymentMethod with selected payment method
payment_method_display_names = {
'stripe': 'Credit/Debit Card (Stripe)',
'paypal': 'PayPal',
'bank_transfer': 'Bank Transfer (Manual)',
'local_wallet': 'Mobile Wallet (Manual)',
}
AccountPaymentMethod.objects.create(
account=account,
type='bank_transfer',
display_name='Bank Transfer (Manual)',
type=payment_method,
display_name=payment_method_display_names.get(payment_method, payment_method.title()),
is_default=True,
is_enabled=True,
is_verified=False,
instructions='Please complete bank transfer and add your reference in Payments.',
instructions='Please complete payment and confirm with your transaction reference.',
)
return user

View File

@@ -521,7 +521,7 @@ class SiteViewSet(AccountModelViewSet):
return Site.objects.filter(account=account)
def perform_create(self, serializer):
"""Create site with account."""
"""Create site with account and auto-grant access to creator."""
account = getattr(self.request, 'account', None)
if not account:
user = self.request.user
@@ -529,7 +529,18 @@ class SiteViewSet(AccountModelViewSet):
account = getattr(user, 'account', None)
# Multiple sites can be active simultaneously - no constraint
serializer.save(account=account)
site = serializer.save(account=account)
# Auto-create SiteUserAccess for owner/admin who creates the site
user = self.request.user
if user and user.is_authenticated and hasattr(user, 'role'):
if user.role in ['owner', 'admin']:
from igny8_core.auth.models import SiteUserAccess
SiteUserAccess.objects.get_or_create(
user=user,
site=site,
defaults={'granted_by': user}
)
def perform_update(self, serializer):
"""Update site."""