payment gateways and plans billing and signup pages refactored

This commit is contained in:
IGNY8 VPS (Salman)
2026-01-07 13:02:53 +00:00
parent ad1756c349
commit ad75fa031e
17 changed files with 4587 additions and 500 deletions

View File

@@ -19,6 +19,7 @@ Endpoints:
"""
import json
import logging
from decimal import Decimal
from django.conf import settings
from django.utils import timezone
from django.db import transaction
@@ -293,6 +294,23 @@ class PayPalCaptureOrderView(APIView):
request=request
)
# IDEMPOTENCY CHECK - Prevent duplicate captures
existing = Payment.objects.filter(
paypal_order_id=order_id,
status='succeeded'
).first()
if existing:
logger.info(f"PayPal order {order_id} already captured as payment {existing.id}")
return success_response(
data={
'status': 'already_captured',
'payment_id': str(existing.id),
'message': 'This order has already been captured'
},
message='Order already captured',
request=request
)
try:
service = PayPalService()
@@ -501,9 +519,8 @@ def paypal_webhook(request):
is_valid = service.verify_webhook_signature(headers, body)
if not is_valid:
logger.warning("PayPal webhook signature verification failed")
# Optionally reject invalid signatures
# return Response({'error': 'Invalid signature'}, status=400)
logger.error("PayPal webhook signature verification failed")
return Response({'error': 'Invalid signature'}, status=status.HTTP_400_BAD_REQUEST)
except PayPalConfigurationError:
logger.warning("PayPal not configured for webhook verification")
@@ -550,6 +567,21 @@ def _process_credit_purchase(account, package_id: str, capture_result: dict) ->
logger.error(f"Credit package {package_id} not found for PayPal capture")
return {'error': 'Package not found'}
# AMOUNT VALIDATION - Prevent price manipulation
captured_amount = Decimal(str(capture_result.get('amount', '0')))
expected_amount = Decimal(str(package.price))
if abs(captured_amount - expected_amount) > Decimal('0.01'):
logger.error(
f"PayPal amount mismatch for package {package_id}: "
f"captured={captured_amount}, expected={expected_amount}"
)
return {
'error': 'Payment amount does not match expected amount',
'captured': str(captured_amount),
'expected': str(expected_amount)
}
with transaction.atomic():
# Create invoice
invoice = InvoiceService.create_credit_package_invoice(

View File

@@ -160,20 +160,18 @@ def initiate_refund(request, payment_id):
def _process_stripe_refund(payment: Payment, amount: Decimal, reason: str) -> bool:
"""Process Stripe refund"""
try:
import stripe
from igny8_core.business.billing.utils.payment_gateways import get_stripe_client
from igny8_core.business.billing.services.stripe_service import StripeService
stripe_client = get_stripe_client()
stripe_service = StripeService()
refund = stripe_client.Refund.create(
payment_intent=payment.stripe_payment_intent_id,
refund = stripe_service.create_refund(
payment_intent_id=payment.stripe_payment_intent_id,
amount=int(amount * 100), # Convert to cents
reason='requested_by_customer',
metadata={'reason': reason}
)
payment.metadata['stripe_refund_id'] = refund.id
return refund.status == 'succeeded'
payment.metadata['stripe_refund_id'] = refund.get('id')
return refund.get('status') == 'succeeded'
except Exception as e:
logger.exception(f"Stripe refund failed for payment {payment.id}: {str(e)}")
@@ -183,25 +181,19 @@ def _process_stripe_refund(payment: Payment, amount: Decimal, reason: str) -> bo
def _process_paypal_refund(payment: Payment, amount: Decimal, reason: str) -> bool:
"""Process PayPal refund"""
try:
from igny8_core.business.billing.utils.payment_gateways import get_paypal_client
from igny8_core.business.billing.services.paypal_service import PayPalService
paypal_client = get_paypal_client()
paypal_service = PayPalService()
refund_request = {
'amount': {
'value': str(amount),
'currency_code': payment.currency
},
'note_to_payer': reason
}
refund = paypal_client.payments.captures.refund(
payment.paypal_capture_id,
refund_request
refund = paypal_service.refund_capture(
capture_id=payment.paypal_capture_id,
amount=float(amount),
currency=payment.currency,
note=reason,
)
payment.metadata['paypal_refund_id'] = refund.id
return refund.status == 'COMPLETED'
payment.metadata['paypal_refund_id'] = refund.get('id')
return refund.get('status') == 'COMPLETED'
except Exception as e:
logger.exception(f"PayPal refund failed for payment {payment.id}: {str(e)}")

View File

@@ -358,6 +358,15 @@ def _handle_checkout_completed(session: dict):
Processes both subscription and one-time credit purchases.
"""
session_id = session.get('id')
# IDEMPOTENCY CHECK - Prevent processing duplicate webhooks
if Payment.objects.filter(
metadata__stripe_checkout_session_id=session_id
).exists():
logger.info(f"Webhook already processed for session {session_id}")
return
metadata = session.get('metadata', {})
account_id = metadata.get('account_id')
payment_type = metadata.get('type', '')