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
@@ -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 -->
|
||||
|
||||
Reference in New Issue
Block a user