feat: implement new payment system with Stripe Elements and PayPal JS SDK
Agent-Logs-Url: https://github.com/dsaub/proyecto-final/sessions/09bd2b8f-753c-4431-816f-eba20606d5a0 Co-authored-by: dsaub <54474838+dsaub@users.noreply.github.com>
This commit is contained in:
committed by
GitHub
parent
7ff014a951
commit
233e42c14e
+603
-3
@@ -4,7 +4,7 @@ from django.contrib.auth import authenticate, login as auth_login, logout as aut
|
||||
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.contrib import messages
|
||||
from .models import User, Product, Category, Cart, CartItem, Image, Order, OrderItem, OrderMessage, ShippingAddress, StockReservation, StockReservationItem, VerificationCode
|
||||
from .models import User, Product, Category, Cart, CartItem, Image, Order, OrderItem, OrderMessage, ShippingAddress, StockReservation, StockReservationItem, VerificationCode, SavedPaymentMethod
|
||||
from . import tasks
|
||||
from .vars import (
|
||||
PAGE_SIZE,
|
||||
@@ -28,6 +28,7 @@ import unicodedata
|
||||
import json
|
||||
import random, string
|
||||
import logging
|
||||
import requests as http_requests
|
||||
# Create your views here.
|
||||
|
||||
|
||||
@@ -97,6 +98,88 @@ def _get_client_ip(request: HttpRequest) -> str:
|
||||
return request.META.get("REMOTE_ADDR", "")
|
||||
|
||||
|
||||
# ==================== PAYPAL ORDERS API v2 HELPERS ====================
|
||||
|
||||
def _get_paypal_base_url() -> str:
|
||||
mode = getattr(settings, "PAYPAL_MODE", "sandbox")
|
||||
if mode == "live":
|
||||
return "https://api-m.paypal.com"
|
||||
return "https://api-m.sandbox.paypal.com"
|
||||
|
||||
|
||||
def _get_paypal_access_token() -> str:
|
||||
"""Obtiene un access token de la API de PayPal."""
|
||||
url = f"{_get_paypal_base_url()}/v1/oauth2/token"
|
||||
response = http_requests.post(
|
||||
url,
|
||||
auth=(settings.PAYPAL_CLIENT_ID, settings.PAYPAL_CLIENT_SECRET),
|
||||
data={"grant_type": "client_credentials"},
|
||||
timeout=15,
|
||||
)
|
||||
response.raise_for_status()
|
||||
return response.json()["access_token"]
|
||||
|
||||
|
||||
def _paypal_create_order(amount_eur: Decimal) -> dict:
|
||||
"""Crea una orden PayPal y retorna el diccionario de respuesta con id y approve_link."""
|
||||
token = _get_paypal_access_token()
|
||||
url = f"{_get_paypal_base_url()}/v2/checkout/orders"
|
||||
headers = {
|
||||
"Content-Type": "application/json",
|
||||
"Authorization": f"Bearer {token}",
|
||||
}
|
||||
payload = {
|
||||
"intent": "CAPTURE",
|
||||
"purchase_units": [
|
||||
{
|
||||
"amount": {
|
||||
"currency_code": "EUR",
|
||||
"value": format(amount_eur, ".2f"),
|
||||
}
|
||||
}
|
||||
],
|
||||
"application_context": {
|
||||
"brand_name": "Comercialmeria",
|
||||
"shipping_preference": "NO_SHIPPING",
|
||||
"user_action": "PAY_NOW",
|
||||
},
|
||||
}
|
||||
response = http_requests.post(url, headers=headers, json=payload, timeout=15)
|
||||
response.raise_for_status()
|
||||
return response.json()
|
||||
|
||||
|
||||
def _paypal_capture_order(order_id: str) -> dict:
|
||||
"""Captura una orden PayPal aprobada por el comprador."""
|
||||
token = _get_paypal_access_token()
|
||||
url = f"{_get_paypal_base_url()}/v2/checkout/orders/{order_id}/capture"
|
||||
headers = {
|
||||
"Content-Type": "application/json",
|
||||
"Authorization": f"Bearer {token}",
|
||||
}
|
||||
response = http_requests.post(url, headers=headers, json={}, timeout=15)
|
||||
response.raise_for_status()
|
||||
return response.json()
|
||||
|
||||
|
||||
# ==================== STRIPE CUSTOMER HELPER ====================
|
||||
|
||||
def _get_or_create_stripe_customer(user) -> str:
|
||||
"""Devuelve el stripe_customer_id del usuario, creando uno nuevo si es necesario."""
|
||||
stripe.api_key = settings.STRIPE_SECRET_KEY
|
||||
existing = SavedPaymentMethod.objects.filter(
|
||||
user=user,
|
||||
method_type=SavedPaymentMethod.TYPE_CARD,
|
||||
stripe_customer_id__gt="",
|
||||
).first()
|
||||
if existing:
|
||||
return existing.stripe_customer_id
|
||||
customer = stripe.Customer.create(
|
||||
email=user.email,
|
||||
name=(f"{user.first_name} {user.last_name}".strip()) or user.username,
|
||||
)
|
||||
return customer.id
|
||||
|
||||
def get_price_with_vat_decimal(price) -> Decimal:
|
||||
"""Retorna un precio con IVA aplicado y redondeado a 2 decimales."""
|
||||
return (Decimal(str(price)) * (Decimal("1") + Decimal(str(VAT_RATE)))).quantize(
|
||||
@@ -1021,12 +1104,18 @@ def checkout(request: HttpRequest):
|
||||
active_reservation_ids = _get_active_reservation_ids_for_request(request)
|
||||
stock_issues = _get_cart_stock_issues(cart_items, exclude_reservation_ids=active_reservation_ids)
|
||||
addresses = ShippingAddress.objects.filter(user=request.user)
|
||||
saved_cards = SavedPaymentMethod.objects.filter(user=request.user, method_type=SavedPaymentMethod.TYPE_CARD)
|
||||
saved_paypal = SavedPaymentMethod.objects.filter(user=request.user, method_type=SavedPaymentMethod.TYPE_PAYPAL).first()
|
||||
return render(request, "tienda/checkout.html", {
|
||||
"cart": cart,
|
||||
"cart_items": cart_items,
|
||||
"addresses": addresses,
|
||||
"stock_issues": stock_issues,
|
||||
"reservation_minutes": STOCK_RESERVATION_MINUTES,
|
||||
"saved_cards": saved_cards,
|
||||
"saved_paypal": saved_paypal,
|
||||
"stripe_publishable_key": settings.STRIPE_PUBLISHABLE_KEY,
|
||||
"paypal_client_id": settings.PAYPAL_CLIENT_ID,
|
||||
})
|
||||
|
||||
@csrf_exempt
|
||||
@@ -1169,7 +1258,28 @@ def search(request: HttpRequest):
|
||||
})
|
||||
|
||||
|
||||
# ==================== PAYPAL PAYMENT ====================
|
||||
def search_suggestions(request: HttpRequest):
|
||||
"""API AJAX que retorna sugerencias de búsqueda en JSON"""
|
||||
query = request.GET.get('q', '').strip()
|
||||
suggestions = []
|
||||
|
||||
if query and len(query) >= 2:
|
||||
products = Product.objects.filter(
|
||||
models.Q(name__icontains=query) |
|
||||
models.Q(briefdesc__icontains=query)
|
||||
).values_list('name', 'id', 'price', 'primary_image_id').distinct()[:8]
|
||||
|
||||
for name, product_id, price, image_id in products:
|
||||
suggestions.append({
|
||||
'name': name,
|
||||
'id': product_id,
|
||||
'price': float(price),
|
||||
})
|
||||
|
||||
return JsonResponse({'suggestions': suggestions})
|
||||
|
||||
|
||||
|
||||
|
||||
@login_required
|
||||
def create_paypal_payment(request: HttpRequest):
|
||||
@@ -1354,7 +1464,497 @@ def paypal_execute(request: HttpRequest):
|
||||
return redirect("checkout")
|
||||
|
||||
|
||||
def search_suggestions(request: HttpRequest):
|
||||
# ==================== STRIPE PAYMENT INTENTS ====================
|
||||
|
||||
@login_required
|
||||
def crear_payment_intent(request: HttpRequest):
|
||||
"""
|
||||
Crea un Stripe PaymentIntent para el carrito actual.
|
||||
Acepta JSON: { shipping_address_id, saved_payment_method_id (opcional), save_card (bool) }
|
||||
"""
|
||||
if request.method != "POST":
|
||||
return JsonResponse({"error": "Método no permitido"}, status=405)
|
||||
|
||||
try:
|
||||
payload = json.loads(request.body.decode("utf-8") or "{}")
|
||||
except (json.JSONDecodeError, UnicodeDecodeError):
|
||||
return JsonResponse({"error": "Cuerpo de la petición inválido"}, status=400)
|
||||
|
||||
shipping_address = _get_selected_shipping_address(request)
|
||||
if shipping_address is None:
|
||||
return JsonResponse({"error": "Debes seleccionar una dirección de envío válida."}, status=400)
|
||||
|
||||
cart = get_or_create_cart(request)
|
||||
cart_items = list(cart.items.select_related("product"))
|
||||
|
||||
if not cart_items:
|
||||
return JsonResponse({"error": "El carrito está vacío"}, status=400)
|
||||
|
||||
active_reservation_ids = _get_active_reservation_ids_for_request(request)
|
||||
stock_issues = _get_cart_stock_issues(cart_items, exclude_reservation_ids=active_reservation_ids)
|
||||
if stock_issues:
|
||||
return JsonResponse({"error": _build_stock_issue_message(stock_issues[0])}, status=400)
|
||||
|
||||
reservation, reservation_issues = _create_stock_reservation_for_cart(
|
||||
request, cart_items, StockReservation.PAYMENT_STRIPE,
|
||||
)
|
||||
if reservation is None:
|
||||
return JsonResponse({"error": reservation_issues[0]}, status=400)
|
||||
|
||||
try:
|
||||
stripe.api_key = settings.STRIPE_SECRET_KEY
|
||||
|
||||
order_total = sum(
|
||||
get_price_with_vat_decimal(item.product.price) * item.quantity
|
||||
for item in cart_items
|
||||
)
|
||||
amount_cents = int(
|
||||
(order_total).quantize(Decimal("0.01"), rounding=ROUND_HALF_UP) * 100
|
||||
)
|
||||
|
||||
pi_params = {
|
||||
"amount": amount_cents,
|
||||
"currency": "eur",
|
||||
"automatic_payment_methods": {"enabled": False},
|
||||
"payment_method_types": ["card"],
|
||||
}
|
||||
|
||||
# If using a saved card, attach customer + payment_method
|
||||
saved_pm_id = payload.get("saved_payment_method_id")
|
||||
if saved_pm_id:
|
||||
saved_pm = SavedPaymentMethod.objects.filter(
|
||||
id=saved_pm_id,
|
||||
user=request.user,
|
||||
method_type=SavedPaymentMethod.TYPE_CARD,
|
||||
).first()
|
||||
if saved_pm is None:
|
||||
return JsonResponse({"error": "Método de pago no encontrado."}, status=400)
|
||||
pi_params["customer"] = saved_pm.stripe_customer_id
|
||||
pi_params["payment_method"] = saved_pm.stripe_payment_method_id
|
||||
|
||||
payment_intent = stripe.PaymentIntent.create(**pi_params)
|
||||
|
||||
request.session[STOCK_RESERVATION_SESSION_KEY] = reservation.id
|
||||
request.session[STOCK_RESERVATION_PAYMENT_SESSION_KEY] = StockReservation.PAYMENT_STRIPE
|
||||
request.session["selected_shipping_address_id"] = shipping_address.id
|
||||
request.session["stripe_save_card"] = bool(payload.get("save_card", False))
|
||||
|
||||
return JsonResponse({
|
||||
"client_secret": payment_intent.client_secret,
|
||||
"payment_intent_id": payment_intent.id,
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
logger.exception("CREATE_PAYMENT_INTENT_ERROR user_id=%s error=%s", request.user.id, str(e))
|
||||
return JsonResponse({"error": f"Error al crear el intento de pago: {str(e)}"}, status=500)
|
||||
|
||||
|
||||
@login_required
|
||||
def confirmar_pago_tarjeta(request: HttpRequest):
|
||||
"""
|
||||
Verificar que el PaymentIntent fue exitoso y crear el pedido.
|
||||
Acepta JSON: { payment_intent_id, payment_method_id (si nueva tarjeta) }
|
||||
"""
|
||||
if request.method != "POST":
|
||||
return JsonResponse({"error": "Método no permitido"}, status=405)
|
||||
|
||||
try:
|
||||
payload = json.loads(request.body.decode("utf-8") or "{}")
|
||||
except (json.JSONDecodeError, UnicodeDecodeError):
|
||||
return JsonResponse({"error": "Cuerpo de la petición inválido"}, status=400)
|
||||
|
||||
payment_intent_id = payload.get("payment_intent_id")
|
||||
if not payment_intent_id:
|
||||
return JsonResponse({"error": "Falta el ID del intento de pago"}, status=400)
|
||||
|
||||
try:
|
||||
stripe.api_key = settings.STRIPE_SECRET_KEY
|
||||
payment_intent = stripe.PaymentIntent.retrieve(payment_intent_id)
|
||||
except Exception as e:
|
||||
logger.exception("RETRIEVE_PAYMENT_INTENT_ERROR user_id=%s error=%s", request.user.id, str(e))
|
||||
return JsonResponse({"error": "Error al verificar el pago"}, status=500)
|
||||
|
||||
if payment_intent.status != "succeeded":
|
||||
return JsonResponse({"error": f"El pago no fue completado (estado: {payment_intent.status})"}, status=400)
|
||||
|
||||
shipping_address_id = request.session.get("selected_shipping_address_id")
|
||||
shipping_address = ShippingAddress.objects.filter(id=shipping_address_id, user=request.user).first()
|
||||
reservation = _get_session_stock_reservation(request, StockReservation.PAYMENT_STRIPE)
|
||||
|
||||
order, order_error = create_order_from_cart(
|
||||
request,
|
||||
Order.PAYMENT_STRIPE,
|
||||
payment_intent_id,
|
||||
shipping_address,
|
||||
stock_reservation=reservation,
|
||||
)
|
||||
|
||||
if order is None:
|
||||
return JsonResponse({"error": order_error}, status=400)
|
||||
|
||||
# Optionally save the card for future use
|
||||
save_card = request.session.pop("stripe_save_card", False)
|
||||
new_payment_method_id = payload.get("payment_method_id")
|
||||
if save_card and new_payment_method_id:
|
||||
try:
|
||||
customer_id = _get_or_create_stripe_customer(request.user)
|
||||
pm = stripe.PaymentMethod.retrieve(new_payment_method_id)
|
||||
stripe.PaymentMethod.attach(new_payment_method_id, customer=customer_id)
|
||||
card = pm.card
|
||||
label = f"{card.brand.capitalize()} •••• {card.last4}"
|
||||
SavedPaymentMethod.objects.create(
|
||||
user=request.user,
|
||||
method_type=SavedPaymentMethod.TYPE_CARD,
|
||||
label=label,
|
||||
stripe_customer_id=customer_id,
|
||||
stripe_payment_method_id=new_payment_method_id,
|
||||
is_default=not SavedPaymentMethod.objects.filter(user=request.user).exists(),
|
||||
)
|
||||
except Exception as e:
|
||||
logger.warning("SAVE_CARD_ERROR user_id=%s error=%s", request.user.id, str(e))
|
||||
|
||||
if "selected_shipping_address_id" in request.session:
|
||||
del request.session["selected_shipping_address_id"]
|
||||
_clear_stock_reservation_session(request)
|
||||
|
||||
return JsonResponse({"success": True, "order_id": order.id, "transaction_code": order.transaction_code})
|
||||
|
||||
|
||||
# ==================== PAYPAL ORDERS API ====================
|
||||
|
||||
@login_required
|
||||
def crear_orden_paypal(request: HttpRequest):
|
||||
"""
|
||||
Crea una orden de PayPal con el total del carrito actual (Orders API v2).
|
||||
Acepta JSON: { shipping_address_id }
|
||||
Retorna: { id: paypal_order_id }
|
||||
"""
|
||||
if request.method != "POST":
|
||||
return JsonResponse({"error": "Método no permitido"}, status=405)
|
||||
|
||||
shipping_address = _get_selected_shipping_address(request)
|
||||
if shipping_address is None:
|
||||
return JsonResponse({"error": "Debes seleccionar una dirección de envío válida."}, status=400)
|
||||
|
||||
cart = get_or_create_cart(request)
|
||||
cart_items = list(cart.items.select_related("product"))
|
||||
|
||||
if not cart_items:
|
||||
return JsonResponse({"error": "El carrito está vacío"}, status=400)
|
||||
|
||||
active_reservation_ids = _get_active_reservation_ids_for_request(request)
|
||||
stock_issues = _get_cart_stock_issues(cart_items, exclude_reservation_ids=active_reservation_ids)
|
||||
if stock_issues:
|
||||
return JsonResponse({"error": _build_stock_issue_message(stock_issues[0])}, status=400)
|
||||
|
||||
reservation, reservation_issues = _create_stock_reservation_for_cart(
|
||||
request, cart_items, StockReservation.PAYMENT_PAYPAL,
|
||||
)
|
||||
if reservation is None:
|
||||
return JsonResponse({"error": reservation_issues[0]}, status=400)
|
||||
|
||||
try:
|
||||
order_total = sum(
|
||||
get_price_with_vat_decimal(item.product.price) * item.quantity
|
||||
for item in cart_items
|
||||
)
|
||||
order_total = order_total.quantize(Decimal("0.01"), rounding=ROUND_HALF_UP)
|
||||
|
||||
paypal_order = _paypal_create_order(order_total)
|
||||
paypal_order_id = paypal_order.get("id")
|
||||
|
||||
request.session["paypal_order_id"] = paypal_order_id
|
||||
request.session["selected_shipping_address_id"] = shipping_address.id
|
||||
request.session[STOCK_RESERVATION_SESSION_KEY] = reservation.id
|
||||
request.session[STOCK_RESERVATION_PAYMENT_SESSION_KEY] = StockReservation.PAYMENT_PAYPAL
|
||||
|
||||
return JsonResponse({"id": paypal_order_id})
|
||||
|
||||
except Exception as e:
|
||||
logger.exception("CREAR_ORDEN_PAYPAL_ERROR user_id=%s error=%s", request.user.id, str(e))
|
||||
return JsonResponse({"error": f"Error al crear la orden de PayPal: {str(e)}"}, status=500)
|
||||
|
||||
|
||||
@login_required
|
||||
def capturar_orden_paypal(request: HttpRequest):
|
||||
"""
|
||||
Captura una orden de PayPal aprobada y crea el pedido en nuestra BD.
|
||||
Acepta JSON: { orderID }
|
||||
"""
|
||||
if request.method != "POST":
|
||||
return JsonResponse({"error": "Método no permitido"}, status=405)
|
||||
|
||||
try:
|
||||
payload = json.loads(request.body.decode("utf-8") or "{}")
|
||||
except (json.JSONDecodeError, UnicodeDecodeError):
|
||||
return JsonResponse({"error": "Cuerpo de la petición inválido"}, status=400)
|
||||
|
||||
paypal_order_id = payload.get("orderID")
|
||||
if not paypal_order_id:
|
||||
return JsonResponse({"error": "Falta el ID de la orden de PayPal"}, status=400)
|
||||
|
||||
# Verify this order belongs to this session
|
||||
session_order_id = request.session.get("paypal_order_id")
|
||||
if session_order_id != paypal_order_id:
|
||||
logger.warning(
|
||||
"PAYPAL_ORDER_MISMATCH user_id=%s session=%s received=%s",
|
||||
request.user.id, session_order_id, paypal_order_id,
|
||||
)
|
||||
return JsonResponse({"error": "ID de orden inválido"}, status=400)
|
||||
|
||||
try:
|
||||
capture_data = _paypal_capture_order(paypal_order_id)
|
||||
except Exception as e:
|
||||
logger.exception("CAPTURAR_ORDEN_PAYPAL_ERROR user_id=%s error=%s", request.user.id, str(e))
|
||||
return JsonResponse({"error": f"Error al capturar el pago de PayPal: {str(e)}"}, status=500)
|
||||
|
||||
capture_status = capture_data.get("status")
|
||||
if capture_status != "COMPLETED":
|
||||
return JsonResponse({"error": f"El pago de PayPal no fue completado (estado: {capture_status})"}, status=400)
|
||||
|
||||
# Extract payer info to optionally save as payment method
|
||||
payer = capture_data.get("payer", {})
|
||||
payer_email = payer.get("email_address", "")
|
||||
payer_id = payer.get("payer_id", "")
|
||||
|
||||
shipping_address_id = request.session.get("selected_shipping_address_id")
|
||||
shipping_address = ShippingAddress.objects.filter(id=shipping_address_id, user=request.user).first()
|
||||
reservation = _get_session_stock_reservation(request, StockReservation.PAYMENT_PAYPAL)
|
||||
|
||||
order, order_error = create_order_from_cart(
|
||||
request,
|
||||
Order.PAYMENT_PAYPAL,
|
||||
paypal_order_id,
|
||||
shipping_address,
|
||||
stock_reservation=reservation,
|
||||
)
|
||||
|
||||
if order is None:
|
||||
return JsonResponse({"error": order_error}, status=400)
|
||||
|
||||
# Save payer info if they want to store the PayPal account (offered in the template)
|
||||
save_paypal = payload.get("save_paypal", False)
|
||||
if save_paypal and payer_email:
|
||||
already_saved = SavedPaymentMethod.objects.filter(
|
||||
user=request.user,
|
||||
method_type=SavedPaymentMethod.TYPE_PAYPAL,
|
||||
paypal_email=payer_email,
|
||||
).exists()
|
||||
if not already_saved:
|
||||
SavedPaymentMethod.objects.create(
|
||||
user=request.user,
|
||||
method_type=SavedPaymentMethod.TYPE_PAYPAL,
|
||||
label=payer_email,
|
||||
paypal_email=payer_email,
|
||||
paypal_payer_id=payer_id,
|
||||
is_default=not SavedPaymentMethod.objects.filter(user=request.user).exists(),
|
||||
)
|
||||
|
||||
if "paypal_order_id" in request.session:
|
||||
del request.session["paypal_order_id"]
|
||||
if "selected_shipping_address_id" in request.session:
|
||||
del request.session["selected_shipping_address_id"]
|
||||
_clear_stock_reservation_session(request)
|
||||
|
||||
return JsonResponse({
|
||||
"success": True,
|
||||
"order_id": order.id,
|
||||
"transaction_code": order.transaction_code,
|
||||
"payer_email": payer_email,
|
||||
})
|
||||
|
||||
|
||||
# ==================== MÉTODOS DE PAGO DEL USUARIO ====================
|
||||
|
||||
@login_required
|
||||
def metodos_pago(request: HttpRequest):
|
||||
"""Lista los métodos de pago guardados del usuario."""
|
||||
metodos = SavedPaymentMethod.objects.filter(user=request.user)
|
||||
return render(request, "tienda/metodos_pago.html", {
|
||||
"metodos": metodos,
|
||||
"paypal_client_id": settings.PAYPAL_CLIENT_ID,
|
||||
})
|
||||
|
||||
|
||||
@login_required
|
||||
def agregar_tarjeta(request: HttpRequest):
|
||||
"""Página para añadir una nueva tarjeta usando Stripe SetupIntent."""
|
||||
return render(request, "tienda/agregar_tarjeta.html", {
|
||||
"stripe_publishable_key": settings.STRIPE_PUBLISHABLE_KEY,
|
||||
})
|
||||
|
||||
|
||||
@login_required
|
||||
def crear_setup_intent(request: HttpRequest):
|
||||
"""
|
||||
Crea un Stripe SetupIntent y retorna el client_secret para que el frontend
|
||||
pueda montar el Card Element y confirmar sin realizar un cobro.
|
||||
"""
|
||||
if request.method != "POST":
|
||||
return JsonResponse({"error": "Método no permitido"}, status=405)
|
||||
try:
|
||||
stripe.api_key = settings.STRIPE_SECRET_KEY
|
||||
customer_id = _get_or_create_stripe_customer(request.user)
|
||||
setup_intent = stripe.SetupIntent.create(
|
||||
customer=customer_id,
|
||||
payment_method_types=["card"],
|
||||
)
|
||||
return JsonResponse({
|
||||
"client_secret": setup_intent.client_secret,
|
||||
"customer_id": customer_id,
|
||||
})
|
||||
except Exception as e:
|
||||
logger.exception("CREATE_SETUP_INTENT_ERROR user_id=%s error=%s", request.user.id, str(e))
|
||||
return JsonResponse({"error": f"Error al iniciar el proceso: {str(e)}"}, status=500)
|
||||
|
||||
|
||||
@login_required
|
||||
def confirmar_setup_intent(request: HttpRequest):
|
||||
"""
|
||||
Tras la confirmación del SetupIntent en el frontend, guarda la tarjeta.
|
||||
Acepta JSON: { payment_method_id, setup_intent_id }
|
||||
"""
|
||||
if request.method != "POST":
|
||||
return JsonResponse({"error": "Método no permitido"}, status=405)
|
||||
|
||||
try:
|
||||
payload = json.loads(request.body.decode("utf-8") or "{}")
|
||||
except (json.JSONDecodeError, UnicodeDecodeError):
|
||||
return JsonResponse({"error": "Cuerpo de la petición inválido"}, status=400)
|
||||
|
||||
payment_method_id = payload.get("payment_method_id")
|
||||
if not payment_method_id:
|
||||
return JsonResponse({"error": "Falta el ID del método de pago"}, status=400)
|
||||
|
||||
try:
|
||||
stripe.api_key = settings.STRIPE_SECRET_KEY
|
||||
customer_id = _get_or_create_stripe_customer(request.user)
|
||||
|
||||
# Attach the PaymentMethod to the customer
|
||||
stripe.PaymentMethod.attach(payment_method_id, customer=customer_id)
|
||||
|
||||
pm = stripe.PaymentMethod.retrieve(payment_method_id)
|
||||
card = pm.card
|
||||
label = f"{card.brand.capitalize()} •••• {card.last4} (exp. {card.exp_month:02d}/{card.exp_year})"
|
||||
|
||||
has_existing = SavedPaymentMethod.objects.filter(user=request.user).exists()
|
||||
saved = SavedPaymentMethod.objects.create(
|
||||
user=request.user,
|
||||
method_type=SavedPaymentMethod.TYPE_CARD,
|
||||
label=label,
|
||||
stripe_customer_id=customer_id,
|
||||
stripe_payment_method_id=payment_method_id,
|
||||
is_default=not has_existing,
|
||||
)
|
||||
|
||||
return JsonResponse({"success": True, "label": label, "id": saved.id})
|
||||
|
||||
except Exception as e:
|
||||
logger.exception("CONFIRMAR_SETUP_INTENT_ERROR user_id=%s error=%s", request.user.id, str(e))
|
||||
return JsonResponse({"error": f"Error al guardar la tarjeta: {str(e)}"}, status=500)
|
||||
|
||||
|
||||
@login_required
|
||||
def eliminar_metodo_pago(request: HttpRequest, id: int):
|
||||
"""Elimina un método de pago guardado del usuario."""
|
||||
if request.method != "POST":
|
||||
messages.error(request, "Acción no permitida.")
|
||||
return redirect("metodos_pago")
|
||||
|
||||
metodo = get_object_or_404(SavedPaymentMethod, id=id, user=request.user)
|
||||
|
||||
# If it's a Stripe card, detach from Stripe too
|
||||
if metodo.method_type == SavedPaymentMethod.TYPE_CARD and metodo.stripe_payment_method_id:
|
||||
try:
|
||||
stripe.api_key = settings.STRIPE_SECRET_KEY
|
||||
stripe.PaymentMethod.detach(metodo.stripe_payment_method_id)
|
||||
except Exception as e:
|
||||
logger.warning("DETACH_PAYMENT_METHOD_ERROR user_id=%s error=%s", request.user.id, str(e))
|
||||
|
||||
metodo.delete()
|
||||
messages.success(request, "Método de pago eliminado correctamente.")
|
||||
return redirect("metodos_pago")
|
||||
|
||||
|
||||
@login_required
|
||||
def agregar_paypal(request: HttpRequest):
|
||||
"""Página para guardar una cuenta de PayPal como método de pago (usa un pago de verificación de 0.01 €)."""
|
||||
return render(request, "tienda/agregar_paypal.html", {
|
||||
"paypal_client_id": settings.PAYPAL_CLIENT_ID,
|
||||
})
|
||||
|
||||
|
||||
@login_required
|
||||
def crear_orden_paypal_setup(request: HttpRequest):
|
||||
"""
|
||||
Crea una orden PayPal de 0.01 € para verificar/guardar la cuenta.
|
||||
Retorna { id: paypal_order_id }
|
||||
"""
|
||||
if request.method != "POST":
|
||||
return JsonResponse({"error": "Método no permitido"}, status=405)
|
||||
try:
|
||||
paypal_order = _paypal_create_order(Decimal("0.01"))
|
||||
return JsonResponse({"id": paypal_order.get("id")})
|
||||
except Exception as e:
|
||||
logger.exception("CREAR_ORDEN_PAYPAL_SETUP_ERROR user_id=%s error=%s", request.user.id, str(e))
|
||||
return JsonResponse({"error": f"Error al iniciar el proceso: {str(e)}"}, status=500)
|
||||
|
||||
|
||||
@login_required
|
||||
def capturar_orden_paypal_setup(request: HttpRequest):
|
||||
"""
|
||||
Captura la orden de verificación de PayPal y guarda la cuenta del usuario.
|
||||
Acepta JSON: { orderID }
|
||||
"""
|
||||
if request.method != "POST":
|
||||
return JsonResponse({"error": "Método no permitido"}, status=405)
|
||||
|
||||
try:
|
||||
payload = json.loads(request.body.decode("utf-8") or "{}")
|
||||
except (json.JSONDecodeError, UnicodeDecodeError):
|
||||
return JsonResponse({"error": "Cuerpo de la petición inválido"}, status=400)
|
||||
|
||||
paypal_order_id = payload.get("orderID")
|
||||
if not paypal_order_id:
|
||||
return JsonResponse({"error": "Falta el ID de la orden"}, status=400)
|
||||
|
||||
try:
|
||||
capture_data = _paypal_capture_order(paypal_order_id)
|
||||
except Exception as e:
|
||||
logger.exception("CAPTURAR_PAYPAL_SETUP_ERROR user_id=%s error=%s", request.user.id, str(e))
|
||||
return JsonResponse({"error": f"Error al verificar la cuenta: {str(e)}"}, status=500)
|
||||
|
||||
if capture_data.get("status") != "COMPLETED":
|
||||
return JsonResponse({"error": "No se pudo verificar la cuenta de PayPal"}, status=400)
|
||||
|
||||
payer = capture_data.get("payer", {})
|
||||
payer_email = payer.get("email_address", "")
|
||||
payer_id = payer.get("payer_id", "")
|
||||
|
||||
if not payer_email:
|
||||
return JsonResponse({"error": "No se pudo obtener el email de PayPal"}, status=400)
|
||||
|
||||
already_saved = SavedPaymentMethod.objects.filter(
|
||||
user=request.user,
|
||||
method_type=SavedPaymentMethod.TYPE_PAYPAL,
|
||||
paypal_email=payer_email,
|
||||
).exists()
|
||||
|
||||
if not already_saved:
|
||||
has_existing = SavedPaymentMethod.objects.filter(user=request.user).exists()
|
||||
SavedPaymentMethod.objects.create(
|
||||
user=request.user,
|
||||
method_type=SavedPaymentMethod.TYPE_PAYPAL,
|
||||
label=payer_email,
|
||||
paypal_email=payer_email,
|
||||
paypal_payer_id=payer_id,
|
||||
is_default=not has_existing,
|
||||
)
|
||||
return JsonResponse({"success": True, "email": payer_email, "already_existed": False})
|
||||
else:
|
||||
return JsonResponse({"success": True, "email": payer_email, "already_existed": True})
|
||||
|
||||
|
||||
"""API AJAX que retorna sugerencias de búsqueda en JSON"""
|
||||
query = request.GET.get('q', '').strip()
|
||||
suggestions = []
|
||||
|
||||
Reference in New Issue
Block a user