Complete Implemenation of tenancy
This commit is contained in:
@@ -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,
|
||||
),
|
||||
]
|
||||
@@ -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',
|
||||
),
|
||||
]
|
||||
@@ -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')
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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."""
|
||||
|
||||
Reference in New Issue
Block a user