feat(billing): add missing payment methods and configurations
- Added migration to include global payment method configurations for Stripe and PayPal (both disabled). - Ensured existing payment methods like bank transfer and manual payment are correctly configured. - Added database constraints and indexes for improved data integrity in billing models. - Introduced foreign key relationship between CreditTransaction and Payment models. - Added webhook configuration fields to PaymentMethodConfig for future payment gateway integrations. - Updated SignUpFormUnified component to handle payment method selection based on user country and plan. - Implemented PaymentHistory component to display user's payment history with status indicators.
This commit is contained in:
@@ -0,0 +1,47 @@
|
||||
# Generated migration to fix subscription constraints
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('igny8_core_auth', '0011_remove_subscription_payment_method'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
# Add unique constraint on tenant_id at database level
|
||||
migrations.RunSQL(
|
||||
sql="""
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS igny8_subscriptions_tenant_id_unique
|
||||
ON igny8_subscriptions(tenant_id);
|
||||
""",
|
||||
reverse_sql="""
|
||||
DROP INDEX IF EXISTS igny8_subscriptions_tenant_id_unique;
|
||||
"""
|
||||
),
|
||||
|
||||
# Make plan field required (non-nullable)
|
||||
# First set default plan (ID 1 - Free Plan) for any null values
|
||||
migrations.RunSQL(
|
||||
sql="""
|
||||
UPDATE igny8_subscriptions
|
||||
SET plan_id = 1
|
||||
WHERE plan_id IS NULL;
|
||||
""",
|
||||
reverse_sql=migrations.RunSQL.noop
|
||||
),
|
||||
|
||||
# Now alter the field to be non-nullable
|
||||
migrations.AlterField(
|
||||
model_name='subscription',
|
||||
name='plan',
|
||||
field=models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.PROTECT,
|
||||
related_name='subscriptions',
|
||||
to='igny8_core_auth.plan',
|
||||
help_text='Subscription plan (tracks historical plan even if account changes plan)'
|
||||
),
|
||||
),
|
||||
]
|
||||
@@ -249,8 +249,6 @@ class Subscription(models.Model):
|
||||
'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(
|
||||
|
||||
@@ -235,6 +235,9 @@ class SiteUserAccessSerializer(serializers.ModelSerializer):
|
||||
read_only_fields = ['granted_at']
|
||||
|
||||
|
||||
from igny8_core.business.billing.models import PAYMENT_METHOD_CHOICES
|
||||
|
||||
|
||||
class UserSerializer(serializers.ModelSerializer):
|
||||
account = AccountSerializer(read_only=True)
|
||||
accessible_sites = serializers.SerializerMethodField()
|
||||
@@ -267,7 +270,7 @@ class RegisterSerializer(serializers.Serializer):
|
||||
)
|
||||
plan_slug = serializers.CharField(max_length=50, required=False)
|
||||
payment_method = serializers.ChoiceField(
|
||||
choices=['stripe', 'paypal', 'bank_transfer', 'local_wallet'],
|
||||
choices=[choice[0] for choice in PAYMENT_METHOD_CHOICES],
|
||||
default='bank_transfer',
|
||||
required=False
|
||||
)
|
||||
@@ -291,6 +294,21 @@ class RegisterSerializer(serializers.Serializer):
|
||||
if 'plan_id' in attrs and attrs.get('plan_id') == '':
|
||||
attrs['plan_id'] = None
|
||||
|
||||
# Validate billing fields for paid plans
|
||||
plan_slug = attrs.get('plan_slug')
|
||||
paid_plans = ['starter', 'growth', 'scale']
|
||||
if plan_slug and plan_slug in paid_plans:
|
||||
# Require billing_country for paid plans
|
||||
if not attrs.get('billing_country'):
|
||||
raise serializers.ValidationError({
|
||||
"billing_country": "Billing country is required for paid plans."
|
||||
})
|
||||
# Require payment_method for paid plans
|
||||
if not attrs.get('payment_method'):
|
||||
raise serializers.ValidationError({
|
||||
"payment_method": "Payment method is required for paid plans."
|
||||
})
|
||||
|
||||
return attrs
|
||||
|
||||
def create(self, validated_data):
|
||||
|
||||
@@ -46,12 +46,36 @@ class RegisterView(APIView):
|
||||
permission_classes = [permissions.AllowAny]
|
||||
|
||||
def post(self, request):
|
||||
from .utils import generate_access_token, generate_refresh_token, get_token_expiry
|
||||
from django.contrib.auth import login
|
||||
|
||||
serializer = RegisterSerializer(data=request.data)
|
||||
if serializer.is_valid():
|
||||
user = serializer.save()
|
||||
|
||||
# Log the user in (create session for session authentication)
|
||||
login(request, user)
|
||||
|
||||
# Get account from user
|
||||
account = getattr(user, 'account', None)
|
||||
|
||||
# Generate JWT tokens
|
||||
access_token = generate_access_token(user, account)
|
||||
refresh_token = generate_refresh_token(user, account)
|
||||
access_expires_at = get_token_expiry('access')
|
||||
refresh_expires_at = get_token_expiry('refresh')
|
||||
|
||||
user_serializer = UserSerializer(user)
|
||||
return success_response(
|
||||
data={'user': user_serializer.data},
|
||||
data={
|
||||
'user': user_serializer.data,
|
||||
'tokens': {
|
||||
'access': access_token,
|
||||
'refresh': refresh_token,
|
||||
'access_expires_at': access_expires_at.isoformat(),
|
||||
'refresh_expires_at': refresh_expires_at.isoformat(),
|
||||
}
|
||||
},
|
||||
message='Registration successful',
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
request=request
|
||||
|
||||
Reference in New Issue
Block a user