AL lPyment methods fully fucntion onstripe and paypal on sandbox
This commit is contained in:
@@ -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):
|
||||
|
||||
@@ -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),
|
||||
),
|
||||
]
|
||||
@@ -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")
|
||||
|
||||
|
||||
@@ -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'),
|
||||
]
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user