AL lPyment methods fully fucntion onstripe and paypal on sandbox

This commit is contained in:
IGNY8 VPS (Salman)
2026-01-20 19:21:01 +00:00
parent c777e5ccb2
commit fa548c3da9
9 changed files with 605 additions and 30 deletions

View File

@@ -161,6 +161,10 @@ class PlanAdmin(ImportExportMixin, Igny8ModelAdmin):
('Stripe Integration', {
'fields': ('stripe_product_id', 'stripe_price_id')
}),
('PayPal Integration', {
'fields': ('paypal_plan_id',),
'description': 'PayPal subscription plan ID (required for PayPal subscriptions)'
}),
)
def bulk_set_active(self, request, queryset):

View File

@@ -0,0 +1,28 @@
# Generated by Django 5.2.10 on 2026-01-20 18:26
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('igny8_core_auth', '0035_account_bonus_credits'),
]
operations = [
migrations.AddField(
model_name='plan',
name='paypal_plan_id',
field=models.CharField(blank=True, help_text='PayPal subscription plan ID', max_length=255, null=True),
),
migrations.AlterField(
model_name='account',
name='status',
field=models.CharField(choices=[('active', 'Active'), ('suspended', 'Suspended'), ('trial', 'Trial'), ('cancelled', 'Cancelled'), ('pending_payment', 'Pending Payment')], default='active', max_length=20),
),
migrations.AlterField(
model_name='historicalaccount',
name='status',
field=models.CharField(choices=[('active', 'Active'), ('suspended', 'Suspended'), ('trial', 'Trial'), ('cancelled', 'Cancelled'), ('pending_payment', 'Pending Payment')], default='active', max_length=20),
),
]

View File

@@ -387,6 +387,9 @@ class Plan(models.Model):
stripe_product_id = models.CharField(max_length=255, blank=True, null=True, help_text="For Stripe plan sync")
stripe_price_id = models.CharField(max_length=255, blank=True, null=True, help_text="Monthly price ID for Stripe")
# PayPal Integration
paypal_plan_id = models.CharField(max_length=255, blank=True, null=True, help_text="PayPal subscription plan ID")
# Legacy field for backward compatibility
credits_per_month = models.IntegerField(default=0, validators=[MinValueValidator(0)], help_text="DEPRECATED: Use included_credits instead")

View File

@@ -30,6 +30,7 @@ from .views.paypal_views import (
PayPalCreateSubscriptionOrderView,
PayPalCaptureOrderView,
PayPalCreateSubscriptionView,
PayPalVerifySubscriptionView,
PayPalReturnVerificationView,
paypal_webhook,
)
@@ -68,6 +69,7 @@ urlpatterns = [
path('paypal/create-subscription-order/', PayPalCreateSubscriptionOrderView.as_view(), name='paypal-create-subscription-order'),
path('paypal/capture-order/', PayPalCaptureOrderView.as_view(), name='paypal-capture-order'),
path('paypal/create-subscription/', PayPalCreateSubscriptionView.as_view(), name='paypal-create-subscription'),
path('paypal/verify-subscription/', PayPalVerifySubscriptionView.as_view(), name='paypal-verify-subscription'),
path('paypal/verify-return/', PayPalReturnVerificationView.as_view(), name='paypal-verify-return'),
path('webhooks/paypal/', paypal_webhook, name='paypal-webhook'),
]

View File

@@ -384,6 +384,240 @@ class PayPalCaptureOrderView(APIView):
)
class PayPalVerifySubscriptionView(APIView):
"""Verify PayPal subscription and activate account"""
permission_classes = [IsAuthenticatedAndActive]
def post(self, request):
"""
Verify PayPal subscription status and activate account if approved.
This is a fallback for when webhooks don't fire or are delayed.
Called by frontend after user returns from PayPal approval.
Request body:
{
"subscription_id": "I-xxxxxxxxxxxxx"
}
Returns:
{
"status": "active",
"subscription_id": "I-...",
"plan_name": "Starter",
"message": "Subscription activated"
}
"""
account = request.user.account
subscription_id = request.data.get('subscription_id')
if not subscription_id:
return error_response(
error='subscription_id is required',
status_code=status.HTTP_400_BAD_REQUEST,
request=request
)
try:
service = PayPalService()
# Get subscription details from PayPal
sub_details = service.get_subscription(subscription_id)
logger.info(
f"PayPal subscription verification for account {account.id}: "
f"subscription={subscription_id}, status={sub_details.get('status')}"
)
paypal_status = sub_details.get('status', '').upper()
# Check if subscription is active/approved
if paypal_status not in ['ACTIVE', 'APPROVED']:
return error_response(
error=f'Subscription not active. Status: {paypal_status}',
status_code=status.HTTP_400_BAD_REQUEST,
request=request
)
# Get PayPal plan ID and find matching Django plan
paypal_plan_id = sub_details.get('plan_id')
plan = Plan.objects.filter(paypal_plan_id=paypal_plan_id).first()
if not plan:
logger.error(f"No plan found with paypal_plan_id={paypal_plan_id}")
return error_response(
error='Plan configuration error',
status_code=status.HTTP_400_BAD_REQUEST,
request=request
)
# Check if already activated (avoid duplicates)
existing_sub = Subscription.objects.filter(
account=account,
external_payment_id=subscription_id,
status='active'
).first()
if existing_sub:
logger.info(f"Subscription {subscription_id} already activated for account {account.id}")
return success_response(
data={
'status': 'active',
'subscription_id': subscription_id,
'plan_name': plan.name,
'already_active': True
},
message='Subscription already active',
request=request
)
# Activate the subscription
with transaction.atomic():
now = timezone.now()
period_end = now + timedelta(days=30)
# Create or update subscription
subscription, created = Subscription.objects.update_or_create(
account=account,
defaults={
'plan': plan,
'status': 'active',
'external_payment_id': subscription_id,
'current_period_start': now,
'current_period_end': period_end,
}
)
# Get or create invoice
invoice, _ = InvoiceService.get_or_create_subscription_invoice(
subscription=subscription,
billing_period_start=now,
billing_period_end=period_end,
)
# Mark invoice as paid
InvoiceService.mark_paid(
invoice=invoice,
payment_method='paypal',
transaction_id=subscription_id
)
# Create payment record
Payment.objects.create(
account=account,
invoice=invoice,
amount=float(plan.price),
currency='USD',
payment_method='paypal',
status='succeeded',
processed_at=now,
approved_at=now,
metadata={
'plan_id': str(plan.id),
'subscription_type': 'paypal_subscription',
'paypal_subscription_id': subscription_id,
'auto_approved': True,
}
)
# Update/create AccountPaymentMethod
from ..models import AccountPaymentMethod
# Clear default from existing methods
AccountPaymentMethod.objects.filter(account=account).update(is_default=False)
# Delete any existing PayPal method
AccountPaymentMethod.objects.filter(account=account, type='paypal').delete()
# Create fresh PayPal payment method
AccountPaymentMethod.objects.create(
account=account,
type='paypal',
display_name='PayPal',
is_default=True,
is_enabled=True,
is_verified=True,
country_code=account.billing_country or '',
metadata={
'last_payment_at': now.isoformat(),
'paypal_subscription_id': subscription_id,
}
)
# Add subscription credits
credits_added = 0
if plan.included_credits and plan.included_credits > 0:
CreditService.add_credits(
account=account,
amount=plan.included_credits,
transaction_type='subscription',
description=f'PayPal Subscription: {plan.name}',
metadata={
'plan_id': str(plan.id),
'payment_method': 'paypal',
}
)
credits_added = plan.included_credits
# Activate account and set plan
update_fields = ['updated_at']
if account.status != 'active':
account.status = 'active'
update_fields.append('status')
if account.plan_id != plan.id:
account.plan = plan
update_fields.append('plan')
if account.payment_method != 'paypal':
account.payment_method = 'paypal'
update_fields.append('payment_method')
account.save(update_fields=update_fields)
logger.info(
f"PayPal subscription activated for account {account.id}: "
f"plan={plan.name}, credits_added={credits_added}"
)
# Send subscription activated email
try:
from igny8_core.business.billing.services.email_service import BillingEmailService
BillingEmailService.send_subscription_activated_email(account, subscription)
except Exception as e:
logger.error(f"Failed to send subscription activated email: {e}")
return success_response(
data={
'status': 'active',
'subscription_id': subscription_id,
'plan_name': plan.name,
'credits_added': credits_added,
},
message='Subscription activated successfully',
request=request
)
except PayPalConfigurationError as e:
logger.error(f"PayPal configuration error: {e}")
return error_response(
error='Payment system not configured',
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
request=request
)
except PayPalAPIError as e:
logger.error(f"PayPal API error verifying subscription: {e}")
return error_response(
error=f'PayPal error: {str(e)}',
status_code=status.HTTP_502_BAD_GATEWAY,
request=request
)
except Exception as e:
logger.exception(f"PayPal subscription verification error: {e}")
return error_response(
error='Failed to verify subscription',
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
request=request
)
class PayPalCreateSubscriptionView(APIView):
"""Create PayPal recurring subscription"""
permission_classes = [IsAuthenticatedAndActive]

View File

@@ -640,10 +640,27 @@ class CreditPackageAdmin(ImportExportMixin, Igny8ModelAdmin):
'bulk_activate',
'bulk_deactivate',
]
actions = [
'bulk_activate',
'bulk_deactivate',
]
fieldsets = (
('Package Info', {
'fields': ('name', 'slug', 'credits', 'price', 'discount_percentage', 'is_active', 'is_featured', 'sort_order')
}),
('Display', {
'fields': ('description', 'features')
}),
('Stripe Integration', {
'fields': ('stripe_product_id', 'stripe_price_id'),
'description': 'Stripe product/price IDs for credit package purchases'
}),
('PayPal Integration', {
'fields': ('paypal_plan_id',),
'description': 'PayPal plan ID for credit package purchases'
}),
('Timestamps', {
'fields': ('created_at', 'updated_at'),
'classes': ('collapse',)
}),
)
def bulk_activate(self, request, queryset):
updated = queryset.update(is_active=True)