Merge pull request #22 from dsaub/development
Enhance stock management, payment systems, and testing coverage
This commit is contained in:
@@ -0,0 +1,59 @@
|
||||
name: Build and Push Docker Image
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- '**' # Esto aplica para cualquier rama
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout del código
|
||||
uses: actions/checkout@v6
|
||||
- name: Configurar Python
|
||||
uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: '3.14'
|
||||
- name: Instalar dependencias
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install -r requirements.txt
|
||||
- name: Ejecutar tests
|
||||
env:
|
||||
DJANGO_SETTINGS_MODULE: proyecto.settings
|
||||
run: |
|
||||
python manage.py test
|
||||
docker:
|
||||
runs-on: ubuntu-latest
|
||||
needs: test
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write # Necesario para subir a GHCR
|
||||
|
||||
steps:
|
||||
- name: Checkout del código
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Configurar Docker Buildx
|
||||
uses: docker/setup-buildx-action@v4
|
||||
|
||||
- name: Login en GHCR
|
||||
uses: docker/login-action@v4
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Preparar tag de imagen
|
||||
run: |
|
||||
TAG=$(echo "${{ github.ref_name }}" | sed 's/\//-/g')
|
||||
echo "IMAGE_TAG=$TAG" >> $GITHUB_ENV
|
||||
|
||||
- name: Build y Push
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
push: true
|
||||
# Sanitizamos el nombre de la rama (reemplazamos / por -)
|
||||
tags: ghcr.io/dsaub/proyecto-mvc:${{ env.IMAGE_TAG }}
|
||||
+1
-10
@@ -4,14 +4,9 @@ ENV PYTHONDONTWRITEBYTECODE=1
|
||||
ENV PYTHONUNBUFFERED=1
|
||||
WORKDIR /app
|
||||
|
||||
RUN adduser -D -S app
|
||||
|
||||
COPY requirements.txt /app/
|
||||
RUN apk --no-cache update && apk --no-cache upgrade
|
||||
RUN apk add --no-cache --virtual .build-deps build-base mariadb-dev libffi-dev \
|
||||
&& apk add --no-cache mariadb-connector-c \
|
||||
&& pip install --no-cache-dir -r requirements.txt \
|
||||
&& apk del .build-deps
|
||||
RUN pip install --no-cache-dir -r requirements.txt
|
||||
|
||||
COPY . /app/
|
||||
RUN chmod +x /app/entrypoint.sh
|
||||
@@ -21,8 +16,4 @@ EXPOSE 8000
|
||||
RUN mkdir -pv /fonts
|
||||
COPY tienda/static/fonts/ /fonts/
|
||||
|
||||
RUN chown -R app: /app /fonts
|
||||
RUN chmod 770 /app/entrypoint.sh
|
||||
USER app
|
||||
|
||||
ENTRYPOINT ["/bin/sh", "/app/entrypoint.sh"]
|
||||
@@ -0,0 +1,6 @@
|
||||
docker:
|
||||
docker build -t ghcr.io/dsaub/proyecto-mvc:development .
|
||||
docker push ghcr.io/dsaub/proyecto-mvc:development
|
||||
|
||||
test:
|
||||
./manage.py test
|
||||
+3
-1
@@ -1,11 +1,13 @@
|
||||
#!/bin/sh
|
||||
|
||||
set -eu
|
||||
|
||||
echo "Sleeping due to mysql..."
|
||||
sleep 10
|
||||
echo "Running DB migrations..."
|
||||
python manage.py migrate
|
||||
echo "Collecting STATIC..."
|
||||
python manage.py collectstatic --noinput
|
||||
python manage.py collectstatic --noinput --clear
|
||||
|
||||
echo "Running server!"
|
||||
|
||||
|
||||
@@ -203,7 +203,11 @@ STORAGES = {
|
||||
'BACKEND': 'django.core.files.storage.FileSystemStorage',
|
||||
},
|
||||
'staticfiles': {
|
||||
'BACKEND': 'whitenoise.storage.CompressedManifestStaticFilesStorage',
|
||||
'BACKEND': (
|
||||
'django.contrib.staticfiles.storage.StaticFilesStorage'
|
||||
if DEBUG
|
||||
else 'whitenoise.storage.CompressedManifestStaticFilesStorage'
|
||||
),
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
+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'],
|
||||
},
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 6.0.4 on 2026-04-17 07:05
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('tienda', '0005_savedpaymentmethod'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='category',
|
||||
name='name',
|
||||
field=models.CharField(max_length=200, unique=True),
|
||||
),
|
||||
]
|
||||
Binary file not shown.
Binary file not shown.
+38
-1
@@ -1,3 +1,5 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from django.db import models
|
||||
from django.contrib.auth.models import User, AbstractUser
|
||||
from django.utils.crypto import get_random_string
|
||||
@@ -253,6 +255,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')
|
||||
@@ -279,4 +316,4 @@ class ShippingAddress(models.Model):
|
||||
# Si se marca como predeterminada, desmarcar las demás del usuario
|
||||
if self.is_default:
|
||||
ShippingAddress.objects.filter(user=self.user, is_default=True).update(is_default=False)
|
||||
super().save(*args, **kwargs)
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
@@ -234,6 +234,46 @@ p.price {
|
||||
object-position: center;
|
||||
}
|
||||
|
||||
/* Estilos para el footer */
|
||||
.footer-link {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
}
|
||||
.footer-link:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
/* Estilos para páginas legales / informativas */
|
||||
.legal-container {
|
||||
max-width: 860px;
|
||||
margin: 2rem auto;
|
||||
padding: 0 1rem;
|
||||
}
|
||||
.legal-container h1 {
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.legal-section {
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
.legal-section h2 {
|
||||
font-size: 1.2rem;
|
||||
margin-bottom: 0.75rem;
|
||||
border-bottom: 1px solid #dee2e6;
|
||||
padding-bottom: 0.25rem;
|
||||
}
|
||||
.legal-section h3 {
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
margin-top: 1rem;
|
||||
margin-bottom: 0.4rem;
|
||||
}
|
||||
|
||||
.legal-footer {
|
||||
margin-top: 2rem;
|
||||
color: #6c757d;
|
||||
}
|
||||
|
||||
.texto-ajustado {
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
@@ -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 %}
|
||||
@@ -0,0 +1,49 @@
|
||||
{% extends "tienda/base.html" %}
|
||||
{% load static %}
|
||||
{% block content %}
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<article class="legal-container">
|
||||
<h1>Aviso Legal</h1>
|
||||
<p>El presente Aviso Legal regula el acceso y uso del sitio web <strong>Comercialmeria</strong>, de conformidad con la Ley 34/2002, de 11 de julio, de Servicios de la Sociedad de la Información y de Comercio Electrónico (LSSICE).</p>
|
||||
|
||||
<section class="legal-section">
|
||||
<h2>1. Datos del Titular</h2>
|
||||
<address>
|
||||
<strong>Denominación:</strong> Comercialmeria<br>
|
||||
<strong>NIF/CIF:</strong> [00000000X]<br>
|
||||
<strong>Domicilio social:</strong> (Dirección de la empresa), Almería, España<br>
|
||||
<strong>Email de contacto:</strong> <a href="mailto:example@example.com">example@example.com</a><br>
|
||||
<strong>Inscripción registral:</strong> [Registro Mercantil de Almería]
|
||||
</address>
|
||||
</section>
|
||||
|
||||
<section class="legal-section">
|
||||
<h2>2. Objeto y Ámbito de Aplicación</h2>
|
||||
<p>Este sitio web ofrece servicios de comercio electrónico local dentro de la provincia de Almería. El acceso y uso de esta web implica la aceptación plena de las condiciones recogidas en este Aviso Legal.</p>
|
||||
</section>
|
||||
|
||||
<section class="legal-section">
|
||||
<h2>3. Propiedad Intelectual e Industrial</h2>
|
||||
<p>Todos los contenidos de esta web —textos, imágenes, logotipos, gráficos, código fuente y diseño— son propiedad de <strong>Comercialmeria</strong> o de sus respectivos titulares, y están protegidos por la legislación vigente en materia de propiedad intelectual e industrial. Queda expresamente prohibida su reproducción, distribución o modificación sin autorización previa por escrito.</p>
|
||||
</section>
|
||||
|
||||
<section class="legal-section">
|
||||
<h2>4. Exclusión de Responsabilidad</h2>
|
||||
<p>Comercialmeria no se responsabiliza de los daños derivados del uso incorrecto del sitio, de interrupciones en el servicio por causas ajenas a nuestra voluntad, ni del contenido de páginas externas enlazadas desde nuestra web.</p>
|
||||
</section>
|
||||
|
||||
<section class="legal-section">
|
||||
<h2>5. Legislación Aplicable y Jurisdicción</h2>
|
||||
<p>Las relaciones establecidas entre el usuario y Comercialmeria se regirán por la legislación española vigente. Para la resolución de cualquier conflicto, ambas partes se someten a los Juzgados y Tribunales de Almería, con renuncia expresa a cualquier otro fuero.</p>
|
||||
</section>
|
||||
|
||||
<footer class="legal-footer">
|
||||
<p><small>Última actualización: Marzo 2026. Comercialmeria — Almería, España.</small></p>
|
||||
</footer>
|
||||
</article>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,82 @@
|
||||
{% extends "tienda/base.html" %}
|
||||
{% load static %}
|
||||
{% block content %}
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<article class="legal-container">
|
||||
<h1>Centro de Ayuda</h1>
|
||||
<p>¿Tienes alguna duda? Aquí encontrarás respuesta a las preguntas más frecuentes sobre cómo funciona <strong>Comercialmeria</strong>.</p>
|
||||
|
||||
<section class="legal-section">
|
||||
<h2>🛒 Compras</h2>
|
||||
|
||||
<h3>¿Cómo realizo un pedido?</h3>
|
||||
<p>Busca el producto que deseas, añádelo al carrito y sigue los pasos del proceso de compra. Necesitarás una cuenta registrada y una dirección de envío en la provincia de Almería.</p>
|
||||
|
||||
<h3>¿Qué métodos de pago se aceptan?</h3>
|
||||
<p>Aceptamos <strong>tarjeta de crédito/débito</strong> a través de Stripe y <strong>PayPal</strong>. Todos los pagos están protegidos con cifrado SSL.</p>
|
||||
|
||||
<h3>¿Puedo modificar un pedido tras confirmarlo?</h3>
|
||||
<p>Una vez confirmado el pedido, no es posible modificarlo directamente. Si necesitas hacer cambios antes de que se envíe, contacta con nosotros lo antes posible en <a href="mailto:example@example.com">example@example.com</a>.</p>
|
||||
|
||||
<h3>¿Cuándo recibiré mi pedido?</h3>
|
||||
<p>El plazo de entrega depende del vendedor. Puedes consultar el estado de tu pedido desde <strong>Mi cuenta → Mis compras</strong>.</p>
|
||||
</section>
|
||||
|
||||
<section class="legal-section">
|
||||
<h2>📦 Envíos</h2>
|
||||
|
||||
<h3>¿A qué zonas se realizan envíos?</h3>
|
||||
<p>Comercialmeria envía únicamente dentro de la <strong>provincia de Almería</strong> (códigos postales que empiecen por 04).</p>
|
||||
|
||||
<h3>¿Cuánto cuesta el envío?</h3>
|
||||
<p>Los gastos de envío se calculan en el momento del checkout y dependen del vendedor y la dirección de entrega.</p>
|
||||
</section>
|
||||
|
||||
<section class="legal-section">
|
||||
<h2>🔄 Devoluciones</h2>
|
||||
|
||||
<h3>¿Puedo devolver un producto?</h3>
|
||||
<p>Sí. Tienes 14 días naturales desde la recepción para solicitar una devolución. Consulta nuestra <a href="{% url 'devoluciones' %}">Política de Devoluciones</a> para más detalles.</p>
|
||||
|
||||
<h3>¿Cuándo recibiré el reembolso?</h3>
|
||||
<p>Una vez aceptada la devolución, el reembolso se tramita en un máximo de 14 días al mismo método de pago utilizado.</p>
|
||||
</section>
|
||||
|
||||
<section class="legal-section">
|
||||
<h2>👤 Mi Cuenta</h2>
|
||||
|
||||
<h3>¿Cómo creo una cuenta?</h3>
|
||||
<p>Haz clic en <strong>Registrarse</strong> en la parte superior de la página, rellena el formulario y verifica tu correo electrónico.</p>
|
||||
|
||||
<h3>He olvidado mi contraseña, ¿qué hago?</h3>
|
||||
<p>Haz clic en <strong>¿Olvidaste tu contraseña?</strong> en la pantalla de inicio de sesión. Recibirás un email con instrucciones para restablecerla.</p>
|
||||
|
||||
<h3>¿Cómo cambio mis datos personales?</h3>
|
||||
<p>Accede a <strong>Mi cuenta → Editar perfil</strong> para actualizar tu nombre, email o contraseña.</p>
|
||||
</section>
|
||||
|
||||
<section class="legal-section">
|
||||
<h2>🏪 Vender en Comercialmeria</h2>
|
||||
|
||||
<h3>¿Cómo puedo vender mis productos?</h3>
|
||||
<p>Regístrate, accede al <strong>Panel Vendedor</strong> desde la barra de navegación y crea tu primer producto. ¡Es gratis y sencillo!</p>
|
||||
|
||||
<h3>¿Hay comisiones?</h3>
|
||||
<p>Actualmente no aplicamos comisiones sobre las ventas. Consulta nuestra sección de <a href="{% url 'terminos' %}">Términos y Condiciones</a> para más información.</p>
|
||||
</section>
|
||||
|
||||
<section class="legal-section">
|
||||
<h2>¿No encuentras respuesta?</h2>
|
||||
<p>Escríbenos a <a href="mailto:example@example.com">example@example.com</a> y te ayudaremos en el menor tiempo posible.</p>
|
||||
</section>
|
||||
|
||||
<footer class="legal-footer">
|
||||
<p><small>Última actualización: Marzo 2026. Comercialmeria — Almería, España.</small></p>
|
||||
</footer>
|
||||
</article>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
@@ -77,7 +77,7 @@
|
||||
</style>
|
||||
{% block head %}{% endblock %}
|
||||
</head>
|
||||
<body>
|
||||
<body class="d-flex flex-column min-vh-100">
|
||||
{% cache 500 sidebar request.user.username %}
|
||||
<nav class="navbar navbar-expand-md header" role="banner">
|
||||
<div class="container-fluid">
|
||||
@@ -135,7 +135,7 @@
|
||||
</nav>
|
||||
{% endcache %}
|
||||
|
||||
<div class="container-fluid" role="main">
|
||||
<div class="container-fluid flex-grow-1 d-flex flex-column" role="main">
|
||||
<!-- Mensajes -->
|
||||
{% if messages %}
|
||||
<div class="row mt-3">
|
||||
@@ -155,26 +155,39 @@
|
||||
|
||||
{% cache 500 footer %}
|
||||
<!-- Footer-->
|
||||
<div id="footer" class="row pt-2 pb-2 mt-5" role="contentinfo">
|
||||
<div class="col-md-12 grid">
|
||||
<p class="text-center">Enlace 1</p>
|
||||
<p class="text-center">Enlace 2</p>
|
||||
<p class="text-center">Enlace 3</p>
|
||||
<p class="text-center">Enlace 4</p>
|
||||
<p class="text-center">Enlace 5</p>
|
||||
<p class="text-center">Enlace 6</p>
|
||||
<p class="text-center">Enlace 7</p>
|
||||
<p class="text-center">Enlace 8</p>
|
||||
<p class="text-center">Enlace 9</p>
|
||||
<p class="text-center">Enlace 10</p>
|
||||
<p class="text-center">Enlace 11</p>
|
||||
<p class="text-center">Enlace 12</p>
|
||||
<p class="text-center">Enlace 13</p>
|
||||
<p class="text-center">Enlace 14</p>
|
||||
<p class="text-center">Enlace 15</p>
|
||||
<p class="text-center">Enlace 16</p>
|
||||
<footer id="footer" class="row pt-4 pb-3 mt-auto" role="contentinfo">
|
||||
<div class="col-12">
|
||||
<div class="row text-center gy-3">
|
||||
<div class="col-12 col-md-4">
|
||||
<h6 class="fw-semibold text-uppercase mb-2">Información Legal</h6>
|
||||
<ul class="list-unstyled mb-0">
|
||||
<li><a href="{% url 'privacidad' %}" class="footer-link">Política de Privacidad</a></li>
|
||||
<li><a href="{% url 'aviso_legal' %}" class="footer-link">Aviso Legal</a></li>
|
||||
<li><a href="{% url 'terminos' %}" class="footer-link">Términos y Condiciones</a></li>
|
||||
<li><a href="{% url 'cookies' %}" class="footer-link">Política de Cookies</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="col-12 col-md-4">
|
||||
<h6 class="fw-semibold text-uppercase mb-2">Compras</h6>
|
||||
<ul class="list-unstyled mb-0">
|
||||
<li><a href="{% url 'devoluciones' %}" class="footer-link">Política de Devoluciones</a></li>
|
||||
<li><a href="{% url 'ayuda' %}" class="footer-link">Centro de Ayuda</a></li>
|
||||
<li><a href="{% url 'ayuda' %}#envios" class="footer-link">Información de Envíos</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="col-12 col-md-4">
|
||||
<h6 class="fw-semibold text-uppercase mb-2">Comercialmeria</h6>
|
||||
<ul class="list-unstyled mb-0">
|
||||
<li><a href="{% url 'sobre_nosotros' %}" class="footer-link">Sobre Nosotros</a></li>
|
||||
<li><a href="{% url 'mis_productos' %}" class="footer-link">Vende con Nosotros</a></li>
|
||||
<li><a href="mailto:example@example.com" class="footer-link">Contacto</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<hr class="mt-3 mb-2">
|
||||
<p class="text-center mb-0" style="font-size: 0.85rem;">© 2026 Comercialmeria — Comercio local en Almería, España</p>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
{% endcache %}
|
||||
</div>
|
||||
{% cache 500 scripts %}
|
||||
@@ -268,4 +281,4 @@
|
||||
</script>
|
||||
{% endcache %}
|
||||
</body>
|
||||
</html>
|
||||
</html>
|
||||
|
||||
@@ -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>
|
||||
@@ -114,42 +104,110 @@
|
||||
</tbody>
|
||||
<tfoot>
|
||||
<tr>
|
||||
<th colspan="2" class="text-end">Subtotal:</th>
|
||||
<th colspan="2" class="text-end">{{ cart.get_total|format_price }}€</th>
|
||||
<th colspan="4" class="text-end">Subtotal:</th>
|
||||
<th class="text-end">{{ cart.get_total|format_price }}€</th>
|
||||
</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>
|
||||
<th colspan="4" class="text-end">IVA (21%):</th>
|
||||
<th class="text-end text-success">+{{ cart.get_vat_amount|format_price }}€</th>
|
||||
</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>
|
||||
<th colspan="4" class="text-end" style="font-size: 1.1rem;">Total:</th>
|
||||
<th class="text-end" style="font-size: 1.1rem;">{{ cart.get_total_with_vat|format_price }}€</th>
|
||||
</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 +215,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 %}
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
{% extends "tienda/base.html" %}
|
||||
{% load static %}
|
||||
{% block content %}
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<article class="legal-container">
|
||||
<h1>Política de Cookies</h1>
|
||||
<p>Esta Política de Cookies explica qué son las cookies, cómo las utiliza <strong>Comercialmeria</strong> y cómo puedes gestionarlas, en cumplimiento del artículo 22.2 de la Ley 34/2002 (LSSICE) y el RGPD.</p>
|
||||
|
||||
<section class="legal-section">
|
||||
<h2>1. ¿Qué son las Cookies?</h2>
|
||||
<p>Las cookies son pequeños ficheros de texto que se almacenan en tu dispositivo (ordenador, tablet o móvil) cuando visitas un sitio web. Su función principal es recordar tus preferencias y mejorar tu experiencia de navegación.</p>
|
||||
</section>
|
||||
|
||||
<section class="legal-section">
|
||||
<h2>2. Tipos de Cookies que Utilizamos</h2>
|
||||
<table class="table table-bordered">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th>Nombre</th>
|
||||
<th>Tipo</th>
|
||||
<th>Finalidad</th>
|
||||
<th>Duración</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><code>sessionid</code></td>
|
||||
<td>Técnica / Sesión</td>
|
||||
<td>Mantiene tu sesión iniciada mientras navegas.</td>
|
||||
<td>Sesión (se elimina al cerrar el navegador)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>csrftoken</code></td>
|
||||
<td>Seguridad</td>
|
||||
<td>Protege los formularios contra ataques CSRF.</td>
|
||||
<td>1 año</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>cart_id</code></td>
|
||||
<td>Funcional</td>
|
||||
<td>Recuerda los productos añadidos al carrito.</td>
|
||||
<td>Sesión</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<p>No utilizamos cookies publicitarias ni de seguimiento de terceros.</p>
|
||||
</section>
|
||||
|
||||
<section class="legal-section">
|
||||
<h2>3. Cookies de Terceros</h2>
|
||||
<p>Al procesar pagos, servicios como <strong>Stripe</strong> o <strong>PayPal</strong> pueden instalar sus propias cookies técnicas para garantizar la seguridad de la transacción. Consulta sus políticas de privacidad para más información.</p>
|
||||
</section>
|
||||
|
||||
<section class="legal-section">
|
||||
<h2>4. ¿Cómo Gestionar las Cookies?</h2>
|
||||
<p>Puedes configurar tu navegador para aceptar, rechazar o eliminar cookies en cualquier momento:</p>
|
||||
<ul>
|
||||
<li><a href="https://support.google.com/chrome/answer/95647" target="_blank" rel="noopener">Google Chrome</a></li>
|
||||
<li><a href="https://support.mozilla.org/es/kb/habilitar-y-deshabilitar-cookies-sitios-web" target="_blank" rel="noopener">Mozilla Firefox</a></li>
|
||||
<li><a href="https://support.apple.com/es-es/guide/safari/sfri11471/mac" target="_blank" rel="noopener">Safari</a></li>
|
||||
<li><a href="https://support.microsoft.com/es-es/windows/eliminar-y-administrar-cookies-168dab11-0753-043d-7c16-ede5947fc64d" target="_blank" rel="noopener">Microsoft Edge</a></li>
|
||||
</ul>
|
||||
<p>Ten en cuenta que deshabilitar ciertas cookies puede afectar a la funcionalidad del sitio (por ejemplo, no podrás mantener la sesión iniciada).</p>
|
||||
</section>
|
||||
|
||||
<section class="legal-section">
|
||||
<h2>5. Consentimiento</h2>
|
||||
<p>Al continuar navegando por nuestra web, aceptas el uso de las cookies técnicas y funcionales descritas anteriormente, necesarias para el correcto funcionamiento del sitio.</p>
|
||||
</section>
|
||||
|
||||
<footer class="legal-footer">
|
||||
<p><small>Última actualización: Marzo 2026. Comercialmeria — Almería, España.</small></p>
|
||||
</footer>
|
||||
</article>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,62 @@
|
||||
{% extends "tienda/base.html" %}
|
||||
{% load static %}
|
||||
{% block content %}
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<article class="legal-container">
|
||||
<h1>Política de Devoluciones</h1>
|
||||
<p>En <strong>Comercialmeria</strong> queremos que estés completamente satisfecho con tu compra. Si por cualquier motivo no es así, aquí te explicamos cómo proceder.</p>
|
||||
|
||||
<section class="legal-section">
|
||||
<h2>1. Plazo de Devolución</h2>
|
||||
<p>Tienes hasta <strong>14 días naturales</strong> desde la recepción del pedido para solicitar una devolución, conforme a lo establecido en el Real Decreto Legislativo 1/2007 (Ley General para la Defensa de los Consumidores y Usuarios).</p>
|
||||
</section>
|
||||
|
||||
<section class="legal-section">
|
||||
<h2>2. Condiciones del Producto</h2>
|
||||
<p>Para que la devolución sea aceptada, el artículo deberá:</p>
|
||||
<ul>
|
||||
<li>Estar en su estado original, sin usar ni manipular.</li>
|
||||
<li>Conservar el embalaje y etiquetas originales.</li>
|
||||
<li>Incluir todos los accesorios, manuales o complementos recibidos.</li>
|
||||
</ul>
|
||||
<p>No se aceptarán devoluciones de productos personalizados, perecederos o descargables una vez utilizados.</p>
|
||||
</section>
|
||||
|
||||
<section class="legal-section">
|
||||
<h2>3. Proceso de Devolución</h2>
|
||||
<ol>
|
||||
<li>Contacta con nosotros por email a <a href="mailto:example@example.com">example@example.com</a> indicando tu número de pedido y el motivo de la devolución.</li>
|
||||
<li>Te enviaremos instrucciones de envío en un plazo máximo de 48 horas laborables.</li>
|
||||
<li>Una vez recibido e inspeccionado el artículo, procesaremos el reembolso.</li>
|
||||
</ol>
|
||||
</section>
|
||||
|
||||
<section class="legal-section">
|
||||
<h2>4. Gastos de Envío en Devoluciones</h2>
|
||||
<p>Los gastos de devolución corren a cargo del comprador, salvo en los siguientes casos:</p>
|
||||
<ul>
|
||||
<li>El producto recibido es defectuoso o incorrecto.</li>
|
||||
<li>Se ha producido un error en el pedido por nuestra parte.</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section class="legal-section">
|
||||
<h2>5. Reembolsos</h2>
|
||||
<p>Una vez aceptada la devolución, el reembolso se realizará en un plazo máximo de <strong>14 días</strong> utilizando el mismo método de pago original. El tiempo de abono dependerá de tu entidad bancaria.</p>
|
||||
</section>
|
||||
|
||||
<section class="legal-section">
|
||||
<h2>6. Productos con Defecto o Daño</h2>
|
||||
<p>Si recibes un producto en mal estado o diferente al pedido, contáctanos en un plazo de <strong>48 horas</strong> tras la entrega adjuntando fotografías. Gestionaremos el reenvío o reembolso sin coste para ti.</p>
|
||||
</section>
|
||||
|
||||
<footer class="legal-footer">
|
||||
<p><small>Última actualización: Marzo 2026. Comercialmeria — Almería, España.</small></p>
|
||||
</footer>
|
||||
</article>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% 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>
|
||||
|
||||
@@ -128,9 +128,11 @@
|
||||
<a href="{% url 'productos' %}" class="btn btn-light btn-lg">
|
||||
🛍️ Explorar Productos
|
||||
</a>
|
||||
{% if not user.is_authenticated %}
|
||||
<a href="{% url 'register' %}" class="btn btn-outline-light btn-lg">
|
||||
📝 Registrarse
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -219,6 +221,7 @@
|
||||
{% endif %}
|
||||
|
||||
<!-- Call to Action -->
|
||||
{% if not user.is_authenticated %}
|
||||
<div class="row mt-5 mb-5">
|
||||
<div class="col-md-12">
|
||||
<div style="background-color: #f8f9fa; padding: 40px; border-radius: 8px; text-align: center;">
|
||||
@@ -232,4 +235,5 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% 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-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,90 @@
|
||||
{% 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">
|
||||
{% with has_card=False %}
|
||||
{% 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 %}
|
||||
{% endwith %}
|
||||
{% if not cards_exist %}
|
||||
<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 paypal_exist %}
|
||||
<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 -->
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{% load static %}
|
||||
{% extends "tienda/base.html" %}
|
||||
{% load static %}
|
||||
{% block content %}
|
||||
|
||||
<div class="row">
|
||||
@@ -61,4 +61,4 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
{% endblock %}
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
{% extends "tienda/base.html" %}
|
||||
{% load static %}
|
||||
{% block content %}
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<article class="legal-container">
|
||||
<h1>Sobre Nosotros</h1>
|
||||
<p><strong>Comercialmeria</strong> es una plataforma de comercio electrónico local nacida con el objetivo de conectar a compradores y vendedores de la provincia de Almería. Creemos en el comercio de proximidad como motor de la economía local.</p>
|
||||
|
||||
<section class="legal-section">
|
||||
<h2>Nuestra Misión</h2>
|
||||
<p>Facilitar que cualquier comerciante o particular de Almería pueda vender sus productos en línea de forma sencilla, segura y sin intermediarios innecesarios, mientras los compradores disfrutan de la comodidad del comercio online con la confianza del trato local.</p>
|
||||
</section>
|
||||
|
||||
<section class="legal-section">
|
||||
<h2>¿Por qué Comercialmeria?</h2>
|
||||
<ul>
|
||||
<li>🌍 <strong>100% local:</strong> Todos los vendedores y envíos son dentro de la provincia de Almería.</li>
|
||||
<li>🔒 <strong>Pagos seguros:</strong> Stripe y PayPal para transacciones protegidas.</li>
|
||||
<li>🚀 <strong>Fácil de usar:</strong> Crea tu tienda en minutos y empieza a vender.</li>
|
||||
<li>💬 <strong>Comunicación directa:</strong> Sistema de mensajería entre comprador y vendedor.</li>
|
||||
<li>📦 <strong>Seguimiento del pedido:</strong> Mantente informado en cada paso del envío.</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section class="legal-section">
|
||||
<h2>Zona de Servicio</h2>
|
||||
<p>Actualmente operamos exclusivamente dentro de la <strong>provincia de Almería</strong>. Esto nos permite garantizar tiempos de entrega más cortos y apoyar directamente al tejido económico local.</p>
|
||||
</section>
|
||||
|
||||
<section class="legal-section">
|
||||
<h2>Contacto</h2>
|
||||
<p>¿Tienes alguna pregunta o propuesta? Escríbenos a <a href="mailto:example@example.com">example@example.com</a> y te responderemos lo antes posible.</p>
|
||||
</section>
|
||||
|
||||
<footer class="legal-footer">
|
||||
<p><small>Comercialmeria — Almería, España. Proyecto Final DAW.</small></p>
|
||||
</footer>
|
||||
</article>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,69 @@
|
||||
{% extends "tienda/base.html" %}
|
||||
{% load static %}
|
||||
{% block content %}
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<article class="legal-container">
|
||||
<h1>Términos y Condiciones de Compra</h1>
|
||||
<p>Las presentes Condiciones Generales de Contratación regulan las transacciones comerciales realizadas a través de <strong>Comercialmeria</strong>, plataforma de comercio local en Almería.</p>
|
||||
|
||||
<section class="legal-section">
|
||||
<h2>1. Proceso de Compra</h2>
|
||||
<ol>
|
||||
<li>Selecciona los productos y añádelos al carrito.</li>
|
||||
<li>Accede a tu cuenta o regístrate para continuar.</li>
|
||||
<li>Revisa el resumen del pedido, incluyendo precios con IVA y gastos de envío.</li>
|
||||
<li>Elige el método de pago y confirma el pedido.</li>
|
||||
<li>Recibirás un email de confirmación con el número de pedido.</li>
|
||||
</ol>
|
||||
</section>
|
||||
|
||||
<section class="legal-section">
|
||||
<h2>2. Precios e IVA</h2>
|
||||
<p>Todos los precios mostrados en la web incluyen el <strong>IVA (21%)</strong> aplicable según la legislación española. Comercialmeria se reserva el derecho a modificar los precios en cualquier momento, si bien los pedidos ya confirmados no se verán afectados.</p>
|
||||
</section>
|
||||
|
||||
<section class="legal-section">
|
||||
<h2>3. Disponibilidad y Stock</h2>
|
||||
<p>La disponibilidad de los productos está sujeta al stock existente en el momento de la compra. En caso de que un artículo no esté disponible tras la confirmación del pedido, te lo comunicaremos de inmediato y procederemos al reembolso íntegro.</p>
|
||||
</section>
|
||||
|
||||
<section class="legal-section">
|
||||
<h2>4. Zona de Envío</h2>
|
||||
<p>Actualmente, Comercialmeria <strong>solo realiza envíos dentro de la provincia de Almería</strong> (códigos postales 04xxx). No se tramitarán pedidos con direcciones de envío fuera de este ámbito.</p>
|
||||
</section>
|
||||
|
||||
<section class="legal-section">
|
||||
<h2>5. Métodos de Pago</h2>
|
||||
<p>Aceptamos los siguientes métodos de pago:</p>
|
||||
<ul>
|
||||
<li>Tarjeta de crédito/débito (Stripe)</li>
|
||||
<li>PayPal</li>
|
||||
</ul>
|
||||
<p>Todos los pagos se procesan a través de plataformas seguras con cifrado SSL. Comercialmeria no almacena datos de tarjetas bancarias.</p>
|
||||
</section>
|
||||
|
||||
<section class="legal-section">
|
||||
<h2>6. Cancelación de Pedidos</h2>
|
||||
<p>Puedes cancelar tu pedido antes de que el vendedor lo marque como enviado. Para ello, contacta con nosotros lo antes posible en <a href="mailto:example@example.com">example@example.com</a>.</p>
|
||||
</section>
|
||||
|
||||
<section class="legal-section">
|
||||
<h2>7. Responsabilidad del Vendedor</h2>
|
||||
<p>Cada producto es ofrecido por vendedores independientes dentro de la plataforma. Comercialmeria actúa como intermediario y no se responsabiliza de la calidad del producto más allá de las garantías legales aplicables.</p>
|
||||
</section>
|
||||
|
||||
<section class="legal-section">
|
||||
<h2>8. Modificación de las Condiciones</h2>
|
||||
<p>Comercialmeria se reserva el derecho a modificar estas condiciones en cualquier momento. Los cambios serán publicados en esta página y resultarán de aplicación desde el momento de su publicación.</p>
|
||||
</section>
|
||||
|
||||
<footer class="legal-footer">
|
||||
<p><small>Última actualización: Marzo 2026. Comercialmeria — Almería, España.</small></p>
|
||||
</footer>
|
||||
</article>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
+1858
-103
File diff suppressed because it is too large
Load Diff
+23
-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,8 +47,24 @@ 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("privacidad", views.rgpd, name="privacidad"),
|
||||
path("devoluciones", views.devoluciones, name="devoluciones"),
|
||||
path("aviso-legal", views.aviso_legal, name="aviso_legal"),
|
||||
path("terminos", views.terminos, name="terminos"),
|
||||
path("cookies", views.cookies, name="cookies"),
|
||||
path("sobre-nosotros", views.sobre_nosotros, name="sobre_nosotros"),
|
||||
path("ayuda", views.ayuda, name="ayuda"),
|
||||
path("reset-password", views.reset_password, name="reset_password"),
|
||||
path("reset-password-phase2/<str:code>", views.reset_password_phase2, name="reset_password_phase2")
|
||||
]
|
||||
|
||||
+673
-25
@@ -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
|
||||
# Create your views here.
|
||||
|
||||
|
||||
@@ -37,6 +38,13 @@ STOCK_RESERVATION_SESSION_KEY = "stock_reservation_id"
|
||||
STOCK_RESERVATION_PAYMENT_SESSION_KEY = "stock_reservation_payment_method"
|
||||
|
||||
|
||||
def _invalidate_product_cache(product_ids):
|
||||
unique_product_ids = {product_id for product_id in product_ids if product_id is not None}
|
||||
if not unique_product_ids:
|
||||
return
|
||||
cache.delete_many([f"product_{product_id}" for product_id in unique_product_ids])
|
||||
|
||||
|
||||
def _normalize_location_text(value: str) -> str:
|
||||
normalized = unicodedata.normalize("NFD", (value or ""))
|
||||
without_accents = "".join(char for char in normalized if unicodedata.category(char) != "Mn")
|
||||
@@ -90,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 = 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 = 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 = 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(
|
||||
@@ -442,6 +532,8 @@ def _create_stock_reservation_for_cart(request: HttpRequest, cart_items, payment
|
||||
for item in cart_items
|
||||
])
|
||||
|
||||
_invalidate_product_cache(product_ids)
|
||||
|
||||
return reservation, []
|
||||
|
||||
|
||||
@@ -593,6 +685,8 @@ def create_order_from_cart(request, payment_method, payment_reference="", shippi
|
||||
product_row.stock -= item.quantity
|
||||
product_row.save(update_fields=["stock"])
|
||||
|
||||
_invalidate_product_cache(product_ids)
|
||||
|
||||
cart.items.all().delete()
|
||||
|
||||
if locked_reservation is not None:
|
||||
@@ -763,10 +857,11 @@ def pedidos_vendedor(request: HttpRequest):
|
||||
pedidos = OrderItem.objects.filter(seller=request.user).select_related(
|
||||
'order', 'product', 'order__buyer', 'order__shipping_address'
|
||||
).prefetch_related('messages__sender').order_by('-created_at')
|
||||
total_pedidos_por_enviar = pedidos.exclude(status=OrderItem.STATUS_SHIPPED).count()
|
||||
|
||||
return render(request, "tienda/pedidos_vendedor.html", {
|
||||
"pedidos": pedidos,
|
||||
"total_pedidos": pedidos.count()
|
||||
"total_pedidos": total_pedidos_por_enviar
|
||||
})
|
||||
|
||||
|
||||
@@ -865,7 +960,10 @@ def crear_producto(request: HttpRequest):
|
||||
name=f"{name}_principal",
|
||||
image=primary_image_file
|
||||
)
|
||||
|
||||
if stock > 4294967295:
|
||||
messages.error(request, "No se puede tener mas de 4294967295 existencias. Por favor, intentelo de nuevo")
|
||||
categories = Category.objects.all()
|
||||
return render(request, "tienda/crear_producto.html", {"categories": categories})
|
||||
# Crear producto
|
||||
producto = Product.objects.create(
|
||||
name=name,
|
||||
@@ -877,6 +975,7 @@ def crear_producto(request: HttpRequest):
|
||||
primary_image=primary_image,
|
||||
creator=request.user
|
||||
)
|
||||
_invalidate_product_cache([producto.id])
|
||||
|
||||
# Agregar imágenes secundarias si se proporcionan
|
||||
if secondary_images_files:
|
||||
@@ -934,6 +1033,13 @@ def editar_producto(request: HttpRequest, id: int):
|
||||
stock = int(stock)
|
||||
if stock < 0:
|
||||
raise ValueError("El stock no puede ser negativo")
|
||||
if stock > 4294967295:
|
||||
messages.error(request, "No se puede tener mas de 4294967295 de stock.")
|
||||
categories = Category.objects.all()
|
||||
return render(request, "tienda/editar_producto.html", {
|
||||
"categories": categories,
|
||||
"producto": producto
|
||||
})
|
||||
except ValueError:
|
||||
messages.error(request, "El stock debe ser un número entero válido.")
|
||||
categories = Category.objects.all()
|
||||
@@ -967,6 +1073,7 @@ def editar_producto(request: HttpRequest, id: int):
|
||||
producto.primary_image = primary_image
|
||||
|
||||
producto.save()
|
||||
_invalidate_product_cache([producto.id])
|
||||
|
||||
if secondary_images_files:
|
||||
producto.secondary_images.clear()
|
||||
@@ -996,6 +1103,7 @@ def borrar_producto(request: HttpRequest, id: int):
|
||||
|
||||
producto = get_object_or_404(Product, id=id, creator=request.user)
|
||||
nombre = producto.name
|
||||
_invalidate_product_cache([producto.id])
|
||||
producto.delete()
|
||||
messages.success(request, f"Producto '{nombre}' eliminado correctamente.")
|
||||
return redirect("mis_productos")
|
||||
@@ -1007,12 +1115,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
|
||||
@@ -1096,7 +1210,7 @@ def create_checkout_session(request: HttpRequest):
|
||||
return JsonResponse({"sessionId": session.id})
|
||||
except Exception as e:
|
||||
logger.exception("STRIPE_CHECKOUT_SESSION_ERROR user_id=%s error=%s", request.user.id, str(e))
|
||||
return JsonResponse({"error": f"Error al crear sesión de pago: {str(e)}"}, status=500)
|
||||
return JsonResponse({"error": "Error al crear la sesión de pago. Por favor inténtalo de nuevo."}, status=500)
|
||||
|
||||
|
||||
@login_required
|
||||
@@ -1155,7 +1269,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):
|
||||
@@ -1340,26 +1475,521 @@ def paypal_execute(request: HttpRequest):
|
||||
return redirect("checkout")
|
||||
|
||||
|
||||
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: # Mínimo 2 caracteres para sugerir
|
||||
# Buscar en nombre (primario) y descripción
|
||||
products = Product.objects.filter(
|
||||
models.Q(name__icontains=query) |
|
||||
models.Q(briefdesc__icontains=query)
|
||||
).values_list('name', 'id', 'price', 'primary_image_id').distinct()[:8] # Máximo 8 sugerencias
|
||||
|
||||
for name, product_id, price, image_id in products:
|
||||
suggestions.append({
|
||||
'name': name,
|
||||
'id': product_id,
|
||||
'price': float(price)
|
||||
})
|
||||
|
||||
return JsonResponse({'suggestions': suggestions})
|
||||
# ==================== 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": "Error al crear el pago. Por favor inténtalo de nuevo."}, 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": "Error al crear la orden de PayPal. Por favor inténtalo de nuevo."}, 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": "Error al capturar el pago de PayPal. Por favor inténtalo de nuevo."}, 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,
|
||||
"cards_exist": metodos.filter(method_type=SavedPaymentMethod.TYPE_CARD).exists(),
|
||||
"paypal_exist": metodos.filter(method_type=SavedPaymentMethod.TYPE_PAYPAL).exists(),
|
||||
})
|
||||
|
||||
|
||||
@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": "Error al iniciar el proceso de configuración. Por favor inténtalo de nuevo."}, 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
|
||||
try:
|
||||
stripe.PaymentMethod.attach(payment_method_id, customer=customer_id)
|
||||
except stripe.error.InvalidRequestError as attach_err:
|
||||
# The payment method may already be attached to a customer
|
||||
pm_check = stripe.PaymentMethod.retrieve(payment_method_id)
|
||||
if pm_check.get("customer") == customer_id:
|
||||
# Already attached to this same customer – continue normally
|
||||
pass
|
||||
else:
|
||||
logger.warning(
|
||||
"CONFIRMAR_SETUP_INTENT_ALREADY_ATTACHED user_id=%s pm=%s error=%s",
|
||||
request.user.id, payment_method_id, str(attach_err),
|
||||
)
|
||||
return JsonResponse(
|
||||
{"error": "Este método de pago ya está asociado a otra cuenta. "
|
||||
"Por favor, usa una tarjeta diferente."},
|
||||
status=400,
|
||||
)
|
||||
|
||||
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})"
|
||||
|
||||
# Avoid saving duplicates in our database
|
||||
existing = SavedPaymentMethod.objects.filter(
|
||||
user=request.user,
|
||||
stripe_payment_method_id=payment_method_id,
|
||||
).first()
|
||||
if existing:
|
||||
return JsonResponse({"success": True, "label": existing.label, "id": existing.id})
|
||||
|
||||
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": "Error al guardar la tarjeta. Por favor inténtalo de nuevo."}, 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": "Error al iniciar la verificación de PayPal. Por favor inténtalo de nuevo."}, 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": "Error al verificar la cuenta de PayPal. Por favor inténtalo de nuevo."}, 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})
|
||||
|
||||
|
||||
# ==================== PORTAL DE USUARIO ====================
|
||||
@@ -1633,6 +2263,24 @@ def reset_password(request: HttpRequest):
|
||||
def rgpd(request: HttpRequest):
|
||||
return render(request, "tienda/rgpd.html", {})
|
||||
|
||||
def devoluciones(request: HttpRequest):
|
||||
return render(request, "tienda/devoluciones.html", {})
|
||||
|
||||
def aviso_legal(request: HttpRequest):
|
||||
return render(request, "tienda/aviso_legal.html", {})
|
||||
|
||||
def terminos(request: HttpRequest):
|
||||
return render(request, "tienda/terminos.html", {})
|
||||
|
||||
def cookies(request: HttpRequest):
|
||||
return render(request, "tienda/cookies.html", {})
|
||||
|
||||
def sobre_nosotros(request: HttpRequest):
|
||||
return render(request, "tienda/sobre_nosotros.html", {})
|
||||
|
||||
def ayuda(request: HttpRequest):
|
||||
return render(request, "tienda/ayuda.html", {})
|
||||
|
||||
def reset_password(request: HttpRequest):
|
||||
if request.method == "GET":
|
||||
return render(request, "tienda/reset_password.html", {})
|
||||
|
||||
Reference in New Issue
Block a user