payment gateways and plans billing and signup pages refactored
This commit is contained in:
@@ -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(
|
||||
|
||||
@@ -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)}")
|
||||
|
||||
@@ -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', '')
|
||||
|
||||
Reference in New Issue
Block a user