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
+9
-2
@@ -1,5 +1,5 @@
|
||||
from django.contrib import admin
|
||||
from .models import Category, Image, Product, Cart, CartItem, Order, OrderItem, OrderMessage, StockReservation, StockReservationItem, User, VerificationCode
|
||||
from .models import Category, Image, Product, Cart, CartItem, Order, OrderItem, OrderMessage, StockReservation, StockReservationItem, User, VerificationCode, SavedPaymentMethod
|
||||
# Register your models here.
|
||||
|
||||
admin.site.register(Category)
|
||||
@@ -86,4 +86,11 @@ class StockReservationAdmin(admin.ModelAdmin):
|
||||
list_display = ('id', 'user', 'session_key', 'status', 'payment_method', 'expires_at', 'created_at')
|
||||
list_filter = ('status', 'payment_method', 'created_at')
|
||||
search_fields = ('user__username', 'user__email', 'session_key')
|
||||
inlines = [StockReservationItemInline]
|
||||
inlines = [StockReservationItemInline]
|
||||
|
||||
|
||||
@admin.register(SavedPaymentMethod)
|
||||
class SavedPaymentMethodAdmin(admin.ModelAdmin):
|
||||
list_display = ('id', 'user', 'method_type', 'label', 'is_default', 'created_at')
|
||||
list_filter = ('method_type', 'is_default', 'created_at')
|
||||
search_fields = ('user__username', 'user__email', 'label', 'paypal_email')
|
||||
@@ -0,0 +1,35 @@
|
||||
# Generated by Django 6.0.1 on 2026-04-10 06:09
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('tienda', '0004_product_stock_stockreservation_stockreservationitem'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='SavedPaymentMethod',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('method_type', models.CharField(choices=[('card', 'Tarjeta'), ('paypal', 'PayPal')], max_length=10)),
|
||||
('label', models.CharField(max_length=200, verbose_name='Etiqueta')),
|
||||
('stripe_customer_id', models.CharField(blank=True, default='', max_length=100)),
|
||||
('stripe_payment_method_id', models.CharField(blank=True, default='', max_length=100)),
|
||||
('paypal_email', models.CharField(blank=True, default='', max_length=254)),
|
||||
('paypal_payer_id', models.CharField(blank=True, default='', max_length=100)),
|
||||
('is_default', models.BooleanField(default=False, verbose_name='Predeterminado')),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='payment_methods', to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Método de pago guardado',
|
||||
'verbose_name_plural': 'Métodos de pago guardados',
|
||||
'ordering': ['-is_default', '-created_at'],
|
||||
},
|
||||
),
|
||||
]
|
||||
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -243,6 +243,41 @@ class OrderMessage(models.Model):
|
||||
return f"Mensaje de {self.sender} - {self.created_at}"
|
||||
|
||||
|
||||
class SavedPaymentMethod(models.Model):
|
||||
"""Métodos de pago guardados por el usuario (tarjetas Stripe o cuentas PayPal)."""
|
||||
TYPE_CARD = "card"
|
||||
TYPE_PAYPAL = "paypal"
|
||||
TYPE_CHOICES = [
|
||||
(TYPE_CARD, "Tarjeta"),
|
||||
(TYPE_PAYPAL, "PayPal"),
|
||||
]
|
||||
|
||||
user = models.ForeignKey(User, on_delete=models.CASCADE, related_name="payment_methods")
|
||||
method_type = models.CharField(max_length=10, choices=TYPE_CHOICES)
|
||||
label = models.CharField(max_length=200, verbose_name="Etiqueta")
|
||||
# Stripe fields
|
||||
stripe_customer_id = models.CharField(max_length=100, blank=True, default="")
|
||||
stripe_payment_method_id = models.CharField(max_length=100, blank=True, default="")
|
||||
# PayPal fields
|
||||
paypal_email = models.CharField(max_length=254, blank=True, default="")
|
||||
paypal_payer_id = models.CharField(max_length=100, blank=True, default="")
|
||||
is_default = models.BooleanField(default=False, verbose_name="Predeterminado")
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
class Meta:
|
||||
verbose_name = "Método de pago guardado"
|
||||
verbose_name_plural = "Métodos de pago guardados"
|
||||
ordering = ["-is_default", "-created_at"]
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.user.username} – {self.label}"
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
if self.is_default:
|
||||
SavedPaymentMethod.objects.filter(user=self.user, is_default=True).update(is_default=False)
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
|
||||
class ShippingAddress(models.Model):
|
||||
"""Direcciones de entrega de los usuarios"""
|
||||
user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='shipping_addresses')
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
{% extends "tienda/base.html" %}
|
||||
{% load static %}
|
||||
|
||||
{% block head %}
|
||||
<script src="https://www.paypal.com/sdk/js?client-id={{ paypal_client_id }}¤cy=EUR"></script>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{% csrf_token %}
|
||||
<div class="row mt-4">
|
||||
<div class="col-12">
|
||||
<h2>Añadir cuenta de PayPal</h2>
|
||||
<nav aria-label="breadcrumb">
|
||||
<ol class="breadcrumb">
|
||||
<li class="breadcrumb-item"><a href="{% url 'portal_usuario' %}">Portal de Usuario</a></li>
|
||||
<li class="breadcrumb-item"><a href="{% url 'metodos_pago' %}">Métodos de Pago</a></li>
|
||||
<li class="breadcrumb-item active">Añadir PayPal</li>
|
||||
</ol>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mt-3 justify-content-center">
|
||||
<div class="col-md-6">
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<p class="text-muted mb-4">
|
||||
Se realizará un pequeño pago de verificación de <strong>0,01 €</strong> para confirmar
|
||||
tu cuenta de PayPal. Tu cuenta quedará guardada para futuras compras.
|
||||
</p>
|
||||
|
||||
<div id="paypal-button-container" class="mb-3"></div>
|
||||
|
||||
<div id="paypal-success" class="alert alert-success d-none">
|
||||
✅ Cuenta de PayPal guardada correctamente.
|
||||
<a href="{% url 'metodos_pago' %}" class="alert-link">Ver mis métodos de pago</a>
|
||||
</div>
|
||||
<div id="paypal-error" class="alert alert-danger d-none"></div>
|
||||
|
||||
<a href="{% url 'metodos_pago' %}" class="btn btn-outline-secondary w-100 mt-3">Cancelar</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const CSRF_TOKEN = document.querySelector('[name=csrfmiddlewaretoken]').value;
|
||||
|
||||
paypal.Buttons({
|
||||
createOrder: async () => {
|
||||
const resp = await fetch('{% url "crear_orden_paypal_setup" %}', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json', 'X-CSRFToken': CSRF_TOKEN },
|
||||
body: JSON.stringify({}),
|
||||
});
|
||||
const data = await resp.json();
|
||||
if (!resp.ok) throw new Error(data.error || 'Error al iniciar la verificación');
|
||||
return data.id;
|
||||
},
|
||||
onApprove: async (data) => {
|
||||
const resp = await fetch('{% url "capturar_orden_paypal_setup" %}', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json', 'X-CSRFToken': CSRF_TOKEN },
|
||||
body: JSON.stringify({ orderID: data.orderID }),
|
||||
});
|
||||
const result = await resp.json();
|
||||
if (!resp.ok) {
|
||||
document.getElementById('paypal-error').textContent = result.error || 'Error al guardar la cuenta';
|
||||
document.getElementById('paypal-error').classList.remove('d-none');
|
||||
return;
|
||||
}
|
||||
const successDiv = document.getElementById('paypal-success');
|
||||
if (result.already_existed) {
|
||||
successDiv.textContent = `La cuenta ${result.email} ya estaba guardada.`;
|
||||
} else {
|
||||
successDiv.textContent = `✅ Cuenta ${result.email} guardada correctamente.`;
|
||||
}
|
||||
successDiv.innerHTML += ' <a href="{% url "metodos_pago" %}" class="alert-link">Ver mis métodos de pago</a>';
|
||||
successDiv.classList.remove('d-none');
|
||||
document.getElementById('paypal-button-container').style.display = 'none';
|
||||
},
|
||||
onError: (err) => {
|
||||
document.getElementById('paypal-error').textContent = 'Error en PayPal: ' + err;
|
||||
document.getElementById('paypal-error').classList.remove('d-none');
|
||||
},
|
||||
}).render('#paypal-button-container');
|
||||
</script>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,130 @@
|
||||
{% extends "tienda/base.html" %}
|
||||
{% load static %}
|
||||
|
||||
{% block head %}
|
||||
<script src="https://js.stripe.com/v3/"></script>
|
||||
<style>
|
||||
#card-element {
|
||||
border: 1px solid #ced4da;
|
||||
border-radius: 0.375rem;
|
||||
padding: 12px;
|
||||
background-color: #fff;
|
||||
}
|
||||
#card-element.StripeElement--focus {
|
||||
border-color: #86b7fe;
|
||||
box-shadow: 0 0 0 0.25rem rgba(13, 110, 253, .25);
|
||||
}
|
||||
#card-element.StripeElement--invalid { border-color: #dc3545; }
|
||||
#card-errors { color: #dc3545; font-size: 0.875rem; margin-top: 4px; }
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{% csrf_token %}
|
||||
<div class="row mt-4">
|
||||
<div class="col-12">
|
||||
<h2>Añadir Tarjeta</h2>
|
||||
<nav aria-label="breadcrumb">
|
||||
<ol class="breadcrumb">
|
||||
<li class="breadcrumb-item"><a href="{% url 'portal_usuario' %}">Portal de Usuario</a></li>
|
||||
<li class="breadcrumb-item"><a href="{% url 'metodos_pago' %}">Métodos de Pago</a></li>
|
||||
<li class="breadcrumb-item active">Añadir Tarjeta</li>
|
||||
</ol>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mt-3 justify-content-center">
|
||||
<div class="col-md-6">
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<p class="text-muted mb-4">
|
||||
Introduce los datos de tu tarjeta. No se realizará ningún cobro ahora; la tarjeta
|
||||
se guardará de forma segura en Stripe para usar en tus próximas compras.
|
||||
</p>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Datos de la tarjeta</label>
|
||||
<div id="card-element"></div>
|
||||
<div id="card-errors" role="alert"></div>
|
||||
</div>
|
||||
|
||||
<button id="save-card-btn" class="btn btn-primary w-100">
|
||||
💳 Guardar tarjeta
|
||||
</button>
|
||||
<div id="save-spinner" class="text-center mt-3 d-none">
|
||||
<div class="spinner-border text-primary" role="status"></div>
|
||||
<p class="text-muted mt-2">Procesando...</p>
|
||||
</div>
|
||||
<div id="save-success" class="alert alert-success mt-3 d-none">
|
||||
✅ Tarjeta guardada correctamente.
|
||||
<a href="{% url 'metodos_pago' %}" class="alert-link">Ver mis métodos de pago</a>
|
||||
</div>
|
||||
|
||||
<a href="{% url 'metodos_pago' %}" class="btn btn-outline-secondary w-100 mt-3">Cancelar</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const CSRF_TOKEN = document.querySelector('[name=csrfmiddlewaretoken]') ?
|
||||
document.querySelector('[name=csrfmiddlewaretoken]').value :
|
||||
document.cookie.split('; ').find(r => r.startsWith('csrftoken='))?.split('=')[1] || '';
|
||||
|
||||
const stripe = Stripe('{{ stripe_publishable_key }}');
|
||||
const elements = stripe.elements();
|
||||
const cardElement = elements.create('card', { hidePostalCode: true });
|
||||
cardElement.mount('#card-element');
|
||||
|
||||
cardElement.on('change', e => {
|
||||
document.getElementById('card-errors').textContent = e.error ? e.error.message : '';
|
||||
});
|
||||
|
||||
document.getElementById('save-card-btn').addEventListener('click', async () => {
|
||||
const btn = document.getElementById('save-card-btn');
|
||||
const spinner = document.getElementById('save-spinner');
|
||||
const errDiv = document.getElementById('card-errors');
|
||||
|
||||
btn.disabled = true;
|
||||
spinner.classList.remove('d-none');
|
||||
errDiv.textContent = '';
|
||||
|
||||
try {
|
||||
// 1. Get SetupIntent client_secret from backend
|
||||
const siResp = await fetch('{% url "crear_setup_intent" %}', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json', 'X-CSRFToken': CSRF_TOKEN },
|
||||
body: JSON.stringify({}),
|
||||
});
|
||||
const siData = await siResp.json();
|
||||
if (!siResp.ok) throw new Error(siData.error || 'Error al iniciar el proceso');
|
||||
|
||||
// 2. Confirm SetupIntent with the card element
|
||||
const { setupIntent, error } = await stripe.confirmCardSetup(siData.client_secret, {
|
||||
payment_method: { card: cardElement },
|
||||
});
|
||||
if (error) throw new Error(error.message);
|
||||
|
||||
// 3. Save to database
|
||||
const saveResp = await fetch('{% url "confirmar_setup_intent" %}', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json', 'X-CSRFToken': CSRF_TOKEN },
|
||||
body: JSON.stringify({ payment_method_id: setupIntent.payment_method }),
|
||||
});
|
||||
const saveData = await saveResp.json();
|
||||
if (!saveResp.ok) throw new Error(saveData.error || 'Error al guardar la tarjeta');
|
||||
|
||||
// 4. Success
|
||||
spinner.classList.add('d-none');
|
||||
document.getElementById('save-success').classList.remove('d-none');
|
||||
btn.style.display = 'none';
|
||||
|
||||
} catch (err) {
|
||||
errDiv.textContent = err.message;
|
||||
btn.disabled = false;
|
||||
spinner.classList.add('d-none');
|
||||
}
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
@@ -4,45 +4,33 @@
|
||||
|
||||
{% block head %}
|
||||
<script src="https://js.stripe.com/v3/"></script>
|
||||
<script src="{% static 'js/checkout.js' %}"></script>
|
||||
<script defer src="https://use.fontawesome.com/releases/v6.4.0/js/all.js"></script>
|
||||
<script src="https://www.paypal.com/sdk/js?client-id={{ paypal_client_id }}¤cy=EUR" defer></script>
|
||||
<style>
|
||||
.payment-methods {
|
||||
display: flex;
|
||||
gap: 15px;
|
||||
flex-wrap: wrap;
|
||||
margin-top: 20px;
|
||||
#card-element {
|
||||
border: 1px solid #ced4da;
|
||||
border-radius: 0.375rem;
|
||||
padding: 12px;
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
.payment-btn {
|
||||
flex: 1;
|
||||
min-width: 200px;
|
||||
padding: 12px 20px;
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
#card-element.StripeElement--focus {
|
||||
border-color: #86b7fe;
|
||||
box-shadow: 0 0 0 0.25rem rgba(13, 110, 253, .25);
|
||||
}
|
||||
|
||||
.payment-section {
|
||||
background-color: #f8f9fa;
|
||||
padding: 30px;
|
||||
border-radius: 8px;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.payment-section h3 {
|
||||
margin-bottom: 20px;
|
||||
color: #212529;
|
||||
#card-element.StripeElement--invalid {
|
||||
border-color: #dc3545;
|
||||
}
|
||||
#card-errors { color: #dc3545; font-size: 0.875rem; margin-top: 4px; }
|
||||
.payment-tab-content { display: none; }
|
||||
.payment-tab-content.active { display: block; }
|
||||
#paypal-button-container { min-height: 50px; }
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
|
||||
{% block content %}
|
||||
<div class="row mt-2">
|
||||
<div class="col-md-12">
|
||||
<!-- Token CSRF para requests AJAX -->
|
||||
{% csrf_token %}
|
||||
|
||||
|
||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||
<h1>Checkout</h1>
|
||||
<a href="{% url 'view_cart' %}" class="btn btn-outline-secondary">← Volver al carrito</a>
|
||||
@@ -66,6 +54,7 @@
|
||||
Si el pago no se completa en ese tiempo, la reserva se cancelará automáticamente.
|
||||
</div>
|
||||
|
||||
<!-- Step 1: Dirección de envío -->
|
||||
<div class="card mb-4">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title mb-3">1) Selecciona la dirección de envío</h5>
|
||||
@@ -90,6 +79,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Resumen del pedido -->
|
||||
<div class="table-responsive mb-4">
|
||||
<table class="table table-striped align-middle">
|
||||
<thead>
|
||||
@@ -116,40 +106,111 @@
|
||||
<tr>
|
||||
<th colspan="2" class="text-end">Subtotal:</th>
|
||||
<th colspan="2" class="text-end">{{ cart.get_total|format_price }}€</th>
|
||||
<td></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th colspan="2" class="text-end">IVA (21%):</th>
|
||||
<th colspan="2" class="text-end text-success">+{{ cart.get_vat_amount|format_price }}€</th>
|
||||
<td></td>
|
||||
</tr>
|
||||
<tr style="background-color: #f8f9fa;">
|
||||
<th colspan="2" class="text-end" style="font-size: 1.1rem;">Total:</th>
|
||||
<th colspan="2" class="text-end" style="font-size: 1.1rem;">{{ cart.get_total_with_vat|format_price }}€</th>
|
||||
<td></td>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="payment-section">
|
||||
<h3>2) Selecciona tu método de pago</h3>
|
||||
<div class="payment-methods">
|
||||
<button
|
||||
id="checkout-button"
|
||||
class="btn btn-primary payment-btn"
|
||||
data-config-url="/tienda/config/"
|
||||
data-session-url="/tienda/create-checkout-session/"
|
||||
{% if not addresses or stock_issues %}disabled{% endif %}>
|
||||
💳 Pagar con Stripe
|
||||
</button>
|
||||
|
||||
<button
|
||||
id="paypal-button"
|
||||
class="btn btn-warning payment-btn"
|
||||
data-payment-url="{% url 'create_paypal_payment' %}"
|
||||
{% if not addresses or stock_issues %}disabled{% endif %}>
|
||||
🅿️ Pagar con PayPal
|
||||
</button>
|
||||
<!-- Step 2: Método de pago -->
|
||||
<div class="card mb-4">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title mb-3">2) Selecciona tu método de pago</h5>
|
||||
|
||||
<!-- Tabs -->
|
||||
<ul class="nav nav-tabs mb-3" id="paymentTabs" role="tablist">
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link active" id="tab-card" data-tab="pane-card" type="button" role="tab">
|
||||
💳 Tarjeta
|
||||
</button>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link" id="tab-paypal" data-tab="pane-paypal" type="button" role="tab">
|
||||
🅿️ PayPal
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<!-- Tarjeta tab -->
|
||||
<div id="pane-card" class="payment-tab-content active">
|
||||
{% if saved_cards %}
|
||||
<div class="mb-3">
|
||||
<p class="fw-semibold">Tarjetas guardadas:</p>
|
||||
{% for card in saved_cards %}
|
||||
<div class="form-check mb-2">
|
||||
<input class="form-check-input" type="radio" name="saved_card_choice" id="card-{{ card.id }}" value="{{ card.id }}" data-pm-id="{{ card.stripe_payment_method_id }}" {% if card.is_default %}checked{% endif %}>
|
||||
<label class="form-check-label" for="card-{{ card.id }}">
|
||||
{{ card.label }}
|
||||
{% if card.is_default %}<span class="badge bg-secondary ms-1">Predeterminada</span>{% endif %}
|
||||
</label>
|
||||
</div>
|
||||
{% endfor %}
|
||||
<div class="form-check mb-3">
|
||||
<input class="form-check-input" type="radio" name="saved_card_choice" id="card-new" value="new">
|
||||
<label class="form-check-label" for="card-new">Usar nueva tarjeta</label>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div id="new-card-section" {% if saved_cards %}style="display:none;"{% endif %}>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Número de tarjeta</label>
|
||||
<div id="card-element"></div>
|
||||
<div id="card-errors" role="alert"></div>
|
||||
</div>
|
||||
<div class="form-check mb-3">
|
||||
<input class="form-check-input" type="checkbox" id="save-card-checkbox">
|
||||
<label class="form-check-label" for="save-card-checkbox">
|
||||
Guardar esta tarjeta para futuras compras
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
id="pay-card-btn"
|
||||
class="btn btn-primary"
|
||||
{% if not addresses or stock_issues %}disabled{% endif %}>
|
||||
💳 Pagar con tarjeta
|
||||
</button>
|
||||
<div id="card-spinner" class="spinner-border spinner-border-sm ms-2 text-primary d-none" role="status"></div>
|
||||
</div>
|
||||
|
||||
<!-- PayPal tab -->
|
||||
<div id="pane-paypal" class="payment-tab-content">
|
||||
{% if saved_paypal %}
|
||||
<div class="alert alert-light border mb-3">
|
||||
<small class="text-muted">Cuenta PayPal guardada:</small>
|
||||
<strong class="d-block">{{ saved_paypal.paypal_email }}</strong>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="form-check mb-3">
|
||||
<input class="form-check-input" type="checkbox" id="save-paypal-checkbox">
|
||||
<label class="form-check-label" for="save-paypal-checkbox">
|
||||
Guardar esta cuenta de PayPal para futuras compras
|
||||
</label>
|
||||
</div>
|
||||
<div id="paypal-button-container"></div>
|
||||
{% if not addresses or stock_issues %}
|
||||
<div class="alert alert-warning mt-2">Selecciona una dirección de envío válida para activar el pago.</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Resultado inline -->
|
||||
<div id="payment-result" class="d-none"></div>
|
||||
|
||||
{% else %}
|
||||
<div class="alert alert-info">Tu carrito está vacío.</div>
|
||||
{% endif %}
|
||||
@@ -157,72 +218,169 @@
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Manejo del botón de PayPal
|
||||
const paypalButton = document.getElementById('paypal-button');
|
||||
if (paypalButton) {
|
||||
paypalButton.addEventListener('click', async function(e) {
|
||||
e.preventDefault();
|
||||
const CSRF_TOKEN = document.querySelector('[name=csrfmiddlewaretoken]').value;
|
||||
const STRIPE_KEY = '{{ stripe_publishable_key }}';
|
||||
const HAS_STOCK_ISSUES = {{ stock_issues|yesno:"true,false" }};
|
||||
const HAS_ADDRESS = {{ addresses|yesno:"true,false" }};
|
||||
|
||||
const shippingAddressSelect = document.getElementById('shipping-address');
|
||||
const selectedShippingAddress = shippingAddressSelect ? shippingAddressSelect.value : '';
|
||||
if (!selectedShippingAddress) {
|
||||
alert('Selecciona una dirección de envío para continuar.');
|
||||
return;
|
||||
}
|
||||
|
||||
const button = this;
|
||||
const originalText = button.innerHTML;
|
||||
button.disabled = true;
|
||||
button.innerHTML = '<span class="spinner-border spinner-border-sm me-2"></span>Procesando...';
|
||||
|
||||
try {
|
||||
// Obtener CSRF token de múltiples formas
|
||||
let csrfToken = document.querySelector('[name=csrfmiddlewaretoken]')?.value;
|
||||
if (!csrfToken) {
|
||||
csrfToken = document.querySelector('input[name="csrfmiddlewaretoken"]')?.value;
|
||||
}
|
||||
if (!csrfToken) {
|
||||
csrfToken = document.cookie.split('; ').find(row => row.startsWith('csrftoken='))?.split('=')[1];
|
||||
}
|
||||
|
||||
console.log('CSRF Token encontrado:', csrfToken ? 'Sí' : 'No');
|
||||
|
||||
const response = await fetch(button.dataset.paymentUrl, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'X-CSRFToken': csrfToken || '',
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ shipping_address_id: selectedShippingAddress })
|
||||
});
|
||||
|
||||
console.log('Response status:', response.status);
|
||||
|
||||
const data = await response.json();
|
||||
console.log('Response data:', data);
|
||||
|
||||
if (response.ok && data.redirect) {
|
||||
// Redirigir a PayPal
|
||||
console.log('Redirigiendo a:', data.redirect);
|
||||
window.location.href = data.redirect;
|
||||
} else if (data.error) {
|
||||
console.error('Error en respuesta:', data.error);
|
||||
alert('Error: ' + data.error);
|
||||
button.disabled = false;
|
||||
button.innerHTML = originalText;
|
||||
} else {
|
||||
console.error('Respuesta inesperada:', data);
|
||||
alert('Error inesperado al procesar el pago');
|
||||
button.disabled = false;
|
||||
button.innerHTML = originalText;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error en fetch:', error);
|
||||
alert('Error al procesar el pago con PayPal: ' + error.message);
|
||||
button.disabled = false;
|
||||
button.innerHTML = originalText;
|
||||
// ---- Tab switching ----
|
||||
document.querySelectorAll('#paymentTabs .nav-link').forEach(btn => {
|
||||
btn.addEventListener('click', () => {
|
||||
document.querySelectorAll('#paymentTabs .nav-link').forEach(b => b.classList.remove('active'));
|
||||
document.querySelectorAll('.payment-tab-content').forEach(p => p.classList.remove('active'));
|
||||
btn.classList.add('active');
|
||||
document.getElementById(btn.dataset.tab).classList.add('active');
|
||||
});
|
||||
});
|
||||
|
||||
// ---- Saved card / new card toggle ----
|
||||
document.querySelectorAll('input[name="saved_card_choice"]').forEach(radio => {
|
||||
radio.addEventListener('change', () => {
|
||||
const newSection = document.getElementById('new-card-section');
|
||||
if (newSection) {
|
||||
newSection.style.display = radio.value === 'new' ? 'block' : 'none';
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// ---- Stripe ----
|
||||
const stripe = Stripe(STRIPE_KEY);
|
||||
const elements = stripe.elements();
|
||||
const cardElement = elements.create('card', { hidePostalCode: true });
|
||||
cardElement.mount('#card-element');
|
||||
cardElement.on('change', e => {
|
||||
document.getElementById('card-errors').textContent = e.error ? e.error.message : '';
|
||||
});
|
||||
|
||||
document.getElementById('pay-card-btn').addEventListener('click', async () => {
|
||||
if (HAS_STOCK_ISSUES || !HAS_ADDRESS) return;
|
||||
|
||||
const addressId = document.getElementById('shipping-address').value;
|
||||
if (!addressId) {
|
||||
alert('Selecciona una dirección de envío para continuar.');
|
||||
return;
|
||||
}
|
||||
|
||||
const btn = document.getElementById('pay-card-btn');
|
||||
const spinner = document.getElementById('card-spinner');
|
||||
btn.disabled = true;
|
||||
spinner.classList.remove('d-none');
|
||||
|
||||
// Determine if using saved card or new card
|
||||
const savedCardRadio = document.querySelector('input[name="saved_card_choice"]:checked');
|
||||
const usingSavedCard = savedCardRadio && savedCardRadio.value !== 'new';
|
||||
const savedPmId = usingSavedCard ? savedCardRadio.dataset.pmId : null;
|
||||
const saveCard = !usingSavedCard && document.getElementById('save-card-checkbox')?.checked;
|
||||
|
||||
try {
|
||||
// 1. Create PaymentIntent
|
||||
const piResp = await fetch('{% url "crear_payment_intent" %}', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json', 'X-CSRFToken': CSRF_TOKEN },
|
||||
body: JSON.stringify({
|
||||
shipping_address_id: addressId,
|
||||
saved_payment_method_id: usingSavedCard ? savedCardRadio.value : null,
|
||||
save_card: saveCard,
|
||||
}),
|
||||
});
|
||||
const piData = await piResp.json();
|
||||
if (!piResp.ok) { throw new Error(piData.error || 'Error al crear el pago'); }
|
||||
|
||||
const clientSecret = piData.client_secret;
|
||||
|
||||
// 2. Confirm payment
|
||||
let confirmResult;
|
||||
if (usingSavedCard) {
|
||||
confirmResult = await stripe.confirmCardPayment(clientSecret, {
|
||||
payment_method: savedPmId,
|
||||
});
|
||||
} else {
|
||||
confirmResult = await stripe.confirmCardPayment(clientSecret, {
|
||||
payment_method: { card: cardElement },
|
||||
});
|
||||
}
|
||||
|
||||
if (confirmResult.error) {
|
||||
throw new Error(confirmResult.error.message);
|
||||
}
|
||||
|
||||
const pi = confirmResult.paymentIntent;
|
||||
if (pi.status !== 'succeeded') {
|
||||
throw new Error('El pago no fue completado.');
|
||||
}
|
||||
|
||||
// 3. Confirm on server
|
||||
const confirmPayload = { payment_intent_id: pi.id };
|
||||
if (!usingSavedCard && saveCard) {
|
||||
confirmPayload.payment_method_id = pi.payment_method;
|
||||
}
|
||||
const confirmResp = await fetch('{% url "confirmar_pago_tarjeta" %}', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json', 'X-CSRFToken': CSRF_TOKEN },
|
||||
body: JSON.stringify(confirmPayload),
|
||||
});
|
||||
const confirmData = await confirmResp.json();
|
||||
if (!confirmResp.ok) { throw new Error(confirmData.error || 'Error al confirmar el pedido'); }
|
||||
|
||||
// 4. Show success
|
||||
showSuccess(confirmData.transaction_code);
|
||||
|
||||
} catch (err) {
|
||||
document.getElementById('card-errors').textContent = err.message;
|
||||
btn.disabled = false;
|
||||
spinner.classList.add('d-none');
|
||||
}
|
||||
});
|
||||
|
||||
// ---- PayPal ----
|
||||
{% if not stock_issues and addresses %}
|
||||
paypal.Buttons({
|
||||
createOrder: async () => {
|
||||
const addressId = document.getElementById('shipping-address').value;
|
||||
if (!addressId) {
|
||||
alert('Selecciona una dirección de envío para continuar.');
|
||||
return Promise.reject(new Error('Sin dirección'));
|
||||
}
|
||||
const resp = await fetch('{% url "crear_orden_paypal" %}', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json', 'X-CSRFToken': CSRF_TOKEN },
|
||||
body: JSON.stringify({ shipping_address_id: addressId }),
|
||||
});
|
||||
const data = await resp.json();
|
||||
if (!resp.ok) { throw new Error(data.error || 'Error al crear la orden'); }
|
||||
return data.id;
|
||||
},
|
||||
onApprove: async (data) => {
|
||||
const savePaypal = document.getElementById('save-paypal-checkbox')?.checked || false;
|
||||
const resp = await fetch('{% url "capturar_orden_paypal" %}', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json', 'X-CSRFToken': CSRF_TOKEN },
|
||||
body: JSON.stringify({ orderID: data.orderID, save_paypal: savePaypal }),
|
||||
});
|
||||
const result = await resp.json();
|
||||
if (!resp.ok) { throw new Error(result.error || 'Error al capturar el pago'); }
|
||||
showSuccess(result.transaction_code);
|
||||
},
|
||||
onError: (err) => {
|
||||
alert('Error en el pago con PayPal: ' + err);
|
||||
},
|
||||
}).render('#paypal-button-container');
|
||||
{% endif %}
|
||||
|
||||
// ---- Success helper ----
|
||||
function showSuccess(transactionCode) {
|
||||
const resultDiv = document.getElementById('payment-result');
|
||||
resultDiv.classList.remove('d-none');
|
||||
resultDiv.innerHTML = `
|
||||
<div class="alert alert-success p-4 text-center">
|
||||
<h4>¡Pago completado!</h4>
|
||||
<p>Tu pedido ha sido procesado correctamente.</p>
|
||||
${transactionCode ? `<p><strong>Código de transacción:</strong> ${transactionCode}</p>` : ''}
|
||||
<a href="{% url 'index' %}" class="btn btn-primary mt-2">Volver a la tienda</a>
|
||||
<a href="{% url 'mis_compras' %}" class="btn btn-outline-primary mt-2 ms-2">Ver mis compras</a>
|
||||
</div>
|
||||
`;
|
||||
window.scrollTo({ top: resultDiv.offsetTop - 20, behavior: 'smooth' });
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
{% endblock %}
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
<a href="{% url 'portal_usuario' %}" class="btn btn-outline-primary">Inicio</a>
|
||||
<a href="{% url 'editar_perfil' %}" class="btn btn-outline-primary">Mi Perfil</a>
|
||||
<a href="{% url 'direcciones_usuario' %}" class="btn btn-primary">Direcciones</a>
|
||||
<a href="{% url 'metodos_pago' %}" class="btn btn-outline-primary">Métodos de Pago</a>
|
||||
<a href="{% url 'mensajes_comprador' %}" class="btn btn-outline-primary">Mensajes</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
<a href="{% url 'portal_usuario' %}" class="btn btn-outline-primary">Inicio</a>
|
||||
<a href="{% url 'editar_perfil' %}" class="btn btn-primary">Mi Perfil</a>
|
||||
<a href="{% url 'direcciones_usuario' %}" class="btn btn-outline-primary">Direcciones</a>
|
||||
<a href="{% url 'metodos_pago' %}" class="btn btn-outline-primary">Métodos de Pago</a>
|
||||
<a href="{% url 'mensajes_comprador' %}" class="btn btn-outline-primary">Mensajes</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
<a href="{% url 'portal_usuario' %}" class="btn btn-outline-primary">Inicio</a>
|
||||
<a href="{% url 'editar_perfil' %}" class="btn btn-outline-primary">Mi Perfil</a>
|
||||
<a href="{% url 'direcciones_usuario' %}" class="btn btn-outline-primary">Direcciones</a>
|
||||
<a href="{% url 'metodos_pago' %}" class="btn btn-outline-primary">Métodos de Pago</a>
|
||||
<a href="{% url 'mensajes_comprador' %}" class="btn btn-primary">Mensajes</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
{% extends "tienda/base.html" %}
|
||||
{% load static %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row mt-4">
|
||||
<div class="col-12">
|
||||
<h2>Métodos de Pago</h2>
|
||||
<nav aria-label="breadcrumb">
|
||||
<ol class="breadcrumb">
|
||||
<li class="breadcrumb-item"><a href="{% url 'portal_usuario' %}">Portal de Usuario</a></li>
|
||||
<li class="breadcrumb-item active">Métodos de Pago</li>
|
||||
</ol>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Menú de navegación del portal -->
|
||||
<div class="row mt-3">
|
||||
<div class="col-12">
|
||||
<div class="btn-group" role="group">
|
||||
<a href="{% url 'portal_usuario' %}" class="btn btn-outline-primary">Inicio</a>
|
||||
<a href="{% url 'mis_compras' %}" class="btn btn-outline-primary">Compras</a>
|
||||
<a href="{% url 'mis_recibos' %}" class="btn btn-outline-primary">Recibos</a>
|
||||
<a href="{% url 'editar_perfil' %}" class="btn btn-outline-primary">Mi Perfil</a>
|
||||
<a href="{% url 'direcciones_usuario' %}" class="btn btn-outline-primary">Direcciones</a>
|
||||
<a href="{% url 'metodos_pago' %}" class="btn btn-primary">Métodos de Pago</a>
|
||||
<a href="{% url 'mensajes_comprador' %}" class="btn btn-outline-primary">Mensajes</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mt-4">
|
||||
<!-- Tarjetas guardadas -->
|
||||
<div class="col-md-6 mb-4">
|
||||
<div class="card h-100">
|
||||
<div class="card-header d-flex justify-content-between align-items-center">
|
||||
<h5 class="mb-0">💳 Tarjetas</h5>
|
||||
<a href="{% url 'agregar_tarjeta' %}" class="btn btn-sm btn-success">➕ Añadir tarjeta</a>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
{% for metodo in metodos %}
|
||||
{% if metodo.method_type == 'card' %}
|
||||
<div class="d-flex justify-content-between align-items-center border rounded p-3 mb-2">
|
||||
<div>
|
||||
<span class="fw-semibold">{{ metodo.label }}</span>
|
||||
{% if metodo.is_default %}<span class="badge bg-secondary ms-2">Predeterminada</span>{% endif %}
|
||||
</div>
|
||||
<form method="POST" action="{% url 'eliminar_metodo_pago' metodo.id %}" onsubmit="return confirm('¿Eliminar esta tarjeta?');">
|
||||
{% csrf_token %}
|
||||
<button type="submit" class="btn btn-sm btn-outline-danger">Eliminar</button>
|
||||
</form>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% if not metodos %}
|
||||
<p class="text-muted mb-0">No tienes tarjetas guardadas.</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Cuentas PayPal guardadas -->
|
||||
<div class="col-md-6 mb-4">
|
||||
<div class="card h-100">
|
||||
<div class="card-header d-flex justify-content-between align-items-center">
|
||||
<h5 class="mb-0">🅿️ PayPal</h5>
|
||||
<a href="{% url 'agregar_paypal' %}" class="btn btn-sm btn-warning">➕ Añadir PayPal</a>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
{% for metodo in metodos %}
|
||||
{% if metodo.method_type == 'paypal' %}
|
||||
<div class="d-flex justify-content-between align-items-center border rounded p-3 mb-2">
|
||||
<div>
|
||||
<span class="fw-semibold">{{ metodo.paypal_email }}</span>
|
||||
{% if metodo.is_default %}<span class="badge bg-secondary ms-2">Predeterminada</span>{% endif %}
|
||||
</div>
|
||||
<form method="POST" action="{% url 'eliminar_metodo_pago' metodo.id %}" onsubmit="return confirm('¿Eliminar esta cuenta de PayPal?');">
|
||||
{% csrf_token %}
|
||||
<button type="submit" class="btn btn-sm btn-outline-danger">Eliminar</button>
|
||||
</form>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% if not metodos %}
|
||||
<p class="text-muted mb-0">No tienes cuentas de PayPal guardadas.</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
@@ -20,6 +20,7 @@
|
||||
<a href="{% url 'portal_usuario' %}" class="btn btn-outline-primary">Inicio</a>
|
||||
<a href="{% url 'mis_compras' %}" class="btn btn-primary">Compras</a>
|
||||
<a href="{% url 'mis_recibos' %}" class="btn btn-outline-primary">Recibos</a>
|
||||
<a href="{% url 'metodos_pago' %}" class="btn btn-outline-primary">Métodos de Pago</a>
|
||||
<a href="{% url 'mensajes_comprador' %}" class="btn btn-outline-primary">Mensajes</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
<a href="{% url 'portal_usuario' %}" class="btn btn-outline-primary">Inicio</a>
|
||||
<a href="{% url 'mis_compras' %}" class="btn btn-outline-primary">Compras</a>
|
||||
<a href="{% url 'mis_recibos' %}" class="btn btn-primary">Recibos</a>
|
||||
<a href="{% url 'metodos_pago' %}" class="btn btn-outline-primary">Métodos de Pago</a>
|
||||
<a href="{% url 'mensajes_comprador' %}" class="btn btn-outline-primary">Mensajes</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
<a href="{% url 'mis_recibos' %}" class="btn btn-outline-primary">Recibos</a>
|
||||
<a href="{% url 'editar_perfil' %}" class="btn btn-outline-primary">Mi Perfil</a>
|
||||
<a href="{% url 'direcciones_usuario' %}" class="btn btn-outline-primary">Direcciones</a>
|
||||
<a href="{% url 'metodos_pago' %}" class="btn btn-outline-primary">Métodos de Pago</a>
|
||||
<a href="{% url 'mensajes_comprador' %}" class="btn btn-outline-primary">Mensajes</a>
|
||||
</div>
|
||||
</div>
|
||||
@@ -67,6 +68,15 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4 mb-3">
|
||||
<div class="card">
|
||||
<div class="card-body text-center">
|
||||
<h5 class="card-title">💳 Métodos de Pago</h5>
|
||||
<p class="text-muted">gestiona tarjetas y cuentas PayPal</p>
|
||||
<a href="{% url 'metodos_pago' %}" class="btn btn-sm btn-primary">Gestionar</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Pedidos recientes -->
|
||||
|
||||
Binary file not shown.
Binary file not shown.
+16
-4
@@ -25,12 +25,15 @@ urlpatterns = [
|
||||
path("cart/remove/<int:item_id>/", views.remove_from_cart, name="remove_from_cart"),
|
||||
path("cart/clear/", views.clear_cart, name="clear_cart"),
|
||||
path("checkout/", views.checkout, name="checkout"),
|
||||
# Stripe
|
||||
path("config/", views.stripe_config, name="stripe_config"),
|
||||
path("create-checkout-session/", views.create_checkout_session, name="create_checkout_session"),
|
||||
# Stripe Payment Intents (nuevo sistema)
|
||||
path("checkout/crear-payment-intent/", views.crear_payment_intent, name="crear_payment_intent"),
|
||||
path("checkout/confirmar-pago-tarjeta/", views.confirmar_pago_tarjeta, name="confirmar_pago_tarjeta"),
|
||||
path("checkout/success/", views.checkout_success, name="checkout_success"),
|
||||
path("checkout/cancel/", views.checkout_cancel, name="checkout_cancel"),
|
||||
# PayPal
|
||||
# PayPal Orders API (nuevo sistema)
|
||||
path("paypal/crear-orden/", views.crear_orden_paypal, name="crear_orden_paypal"),
|
||||
path("paypal/capturar-orden/", views.capturar_orden_paypal, name="capturar_orden_paypal"),
|
||||
# PayPal (legacy - mantenido por compatibilidad)
|
||||
path("paypal/create-payment/", views.create_paypal_payment, name="create_paypal_payment"),
|
||||
path("paypal/execute/", views.paypal_execute, name="paypal_execute"),
|
||||
# Portal de usuario
|
||||
@@ -44,6 +47,15 @@ urlpatterns = [
|
||||
path("usuario/direcciones/<int:id>/editar/", views.editar_direccion, name="editar_direccion"),
|
||||
path("usuario/direcciones/<int:id>/eliminar/", views.eliminar_direccion, name="eliminar_direccion"),
|
||||
path("usuario/mensajes/", views.mensajes_comprador, name="mensajes_comprador"),
|
||||
# Métodos de pago del usuario
|
||||
path("usuario/metodos-pago/", views.metodos_pago, name="metodos_pago"),
|
||||
path("usuario/metodos-pago/agregar-tarjeta/", views.agregar_tarjeta, name="agregar_tarjeta"),
|
||||
path("usuario/metodos-pago/agregar-tarjeta/crear-setup-intent/", views.crear_setup_intent, name="crear_setup_intent"),
|
||||
path("usuario/metodos-pago/agregar-tarjeta/confirmar/", views.confirmar_setup_intent, name="confirmar_setup_intent"),
|
||||
path("usuario/metodos-pago/<int:id>/eliminar/", views.eliminar_metodo_pago, name="eliminar_metodo_pago"),
|
||||
path("usuario/metodos-pago/agregar-paypal/", views.agregar_paypal, name="agregar_paypal"),
|
||||
path("usuario/metodos-pago/agregar-paypal/crear-orden/", views.crear_orden_paypal_setup, name="crear_orden_paypal_setup"),
|
||||
path("usuario/metodos-pago/agregar-paypal/capturar/", views.capturar_orden_paypal_setup, name="capturar_orden_paypal_setup"),
|
||||
path("verify/<str:code>", views.verify, name="verify"),
|
||||
path("rgpd", views.rgpd, name="rgpd"),
|
||||
path("reset-password", views.reset_password, name="reset_password"),
|
||||
|
||||
+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