feat: lots of stuff

This commit is contained in:
2024-03-24 20:04:02 +01:00
parent 22cea344a5
commit 7725247a3f
11 changed files with 360 additions and 75 deletions
+22
View File
@@ -0,0 +1,22 @@
name: CI
on: [push]
jobs:
ci:
strategy:
fail-fast: false
matrix:
python-version: ["3.9", "3.10", "3.11"]
os: [ubuntu-20.04]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v2
- uses: actions/setup-python@v2
with:
python-version: ${{ matrix.python-version }}
- name: Setup dependencies
run: pip install -r requirements.test.txt
- name: Run tests
env:
DJANGO_SETTINGS_MODULE: config.settings.develop
run: pytest --cov && coverage xml
+63 -5
View File
@@ -1,11 +1,69 @@
# Django Template # Shop
## How to use the template? ## Desarrollo
Just run this, changing the project name: ### Establecer variables de entorno
A partir del archivo `.env.example`, creamos un archivo `.env` en el que podemos establecer las
[variables de entorno](#variables-de-entorno).
### Instalar dependencias
```bash ```bash
django-admin startproject --template=https://github.com/pablo-moreno/django-template/archive/refs/heads/main.zip <project name> pip install -r requirements.test.txt
``` ```
- Django's extensions already installed (such as the beautiful shell plus) ### Lanzar migraciones
```bash
python manage.py migrate
```
### Arrancar servidor
```bash
python manage.py runserver
```
### Tareas de Celery
```bash
celery -A config worker -B -l debug
```
### Pruebas con pytest
```bash
pytest --cov
```
## Variables de entorno
Puedes crear un archivo `.env` con las siguientes variables de entorno.
| Variable | Default value | Comments |
|-------------------------|----------------------------------|--------------------------------------------|
| SECRET_KEY | secret key | Clave de encriptación |
| DEBUG | TRUE | Modo de desarrollo activo |
| DATABASE_URL | sqlite:///db.sqlite3 | URL de conexión a base de datos |
| STATIC_ROOT | static | Carpeta de estáticos |
| MEDIA_ROOT | media | Carpeta de archivos media |
| REDIS_HOST | redis | Dirección de servidor Redis |
| REDIS_PORT | 6379 | Puerto de Redis |
| PAGE_SIZE | 20 | Tamaño de paginación |
| ALLOWED_HOSTS | * | Hosts permitidos (CORS) |
| CELERY_BROKER_URL | redis://172.17.0.1:6379/0 | Url de conexión para celery |
| S3_ENABLED | FALSE | Activado protocolo S3 |
| S3_HOST | | URL de servidor S3 |
| AWS_ACCESS_KEY_ID | | Clave de acceso de S3 |
| AWS_SECRET_ACCESS_KEY | | Clave secreta de S3 |
| AWS_STORAGE_BUCKET_NAME | | Nombre del bucket de S3 |
| AWS_S3_ENDPOINT_URL | | Endpoint de S3 |
| REDSYS_SHARED_SECRET | sq7HjrUOBfKmC576ILgskD5srU870gJ7 | Clave privada de Redsys |
| REDSYS_MERCHANT_CODE | 999008881 | Código de negocio de Redsys |
| REDSYS_TERMINAL | 001 | Terminal de Redsys |
| REDSYS_CURRENCY_CODE | 978 | Código de moneda de Redsys |
| REDSYS_TPV_DOMAIN | | Dominio del TPV para redirección de Redsys |
-11
View File
@@ -177,14 +177,3 @@ SPECTACULAR_SETTINGS = {
CELERY_BROKER_URL = env.str("CELERY_BROKER_URL", default="redis://172.17.0.1:6379/0") CELERY_BROKER_URL = env.str("CELERY_BROKER_URL", default="redis://172.17.0.1:6379/0")
CELERY_TIME_ZONE = TIME_ZONE CELERY_TIME_ZONE = TIME_ZONE
CELERY_ALWAYS_EAGER = DEBUG CELERY_ALWAYS_EAGER = DEBUG
# Redsys
# https://pagosonline.redsys.es/conexion-redireccion.html
REDSYS_SHARED_SECRET = env.str(
"REDSYS_SHARED_SECRET", "sq7HjrUOBfKmC576ILgskD5srU870gJ7"
)
REDSYS_MERCHANT_CODE = env.str("REDSYS_MERCHANT_CODE", "999008881")
REDSYS_TERMINAL = env.str("REDSYS_TERMINAL", "001")
REDSYS_CURRENCY_CODE = env.str("REDSYS_CURRENCY_CODE", "978")
REDSYS_TPV_DOMAIN = env.str("REDSYS_TPV_DOMAIN", "")
+13
View File
@@ -25,6 +25,8 @@ REDIS_PORT = env.str("REDIS_PORT", 6379)
PAGE_SIZE = env.int("PAGE_SIZE", 20) PAGE_SIZE = env.int("PAGE_SIZE", 20)
ALLOWED_HOSTS = env.str("ALLOWED_HOSTS", "*").split(",") ALLOWED_HOSTS = env.str("ALLOWED_HOSTS", "*").split(",")
CELERY_BROKER_URL = env.str("CELERY_BROKER_URL", default="redis://172.17.0.1:6379/0")
S3_ENABLED = env.bool("S3_ENABLED", False) S3_ENABLED = env.bool("S3_ENABLED", False)
AWS_S3_HOST = env.str("S3_HOST", "") AWS_S3_HOST = env.str("S3_HOST", "")
@@ -32,3 +34,14 @@ AWS_ACCESS_KEY_ID = env.str("S3_ACCESS_KEY_ID", "")
AWS_SECRET_ACCESS_KEY = env.str("S3_SECRET_ACCESS_KEY", "") AWS_SECRET_ACCESS_KEY = env.str("S3_SECRET_ACCESS_KEY", "")
AWS_STORAGE_BUCKET_NAME = env.str("S3_STORAGE_BUCKET_NAME", "") AWS_STORAGE_BUCKET_NAME = env.str("S3_STORAGE_BUCKET_NAME", "")
AWS_S3_ENDPOINT_URL = env.str("S3_ENDPOINT_URL", "") AWS_S3_ENDPOINT_URL = env.str("S3_ENDPOINT_URL", "")
# Redsys
# https://pagosonline.redsys.es/conexion-redireccion.html
REDSYS_SHARED_SECRET = env.str(
"REDSYS_SHARED_SECRET", "sq7HjrUOBfKmC576ILgskD5srU870gJ7"
)
REDSYS_MERCHANT_CODE = env.str("REDSYS_MERCHANT_CODE", "999008881")
REDSYS_TERMINAL = env.str("REDSYS_TERMINAL", "001")
REDSYS_CURRENCY_CODE = env.str("REDSYS_CURRENCY_CODE", "978")
REDSYS_TPV_DOMAIN = env.str("REDSYS_TPV_DOMAIN", "")
+100 -1
View File
@@ -1,4 +1,4 @@
# Generated by Django 5.0.3 on 2024-03-24 12:51 # Generated by Django 5.0.3 on 2024-03-24 18:23
import django.db.models.deletion import django.db.models.deletion
import django.utils.timezone import django.utils.timezone
@@ -13,6 +13,43 @@ class Migration(migrations.Migration):
dependencies = [] dependencies = []
operations = [ operations = [
migrations.CreateModel(
name="Customer",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
(
"vat_id",
models.CharField(
max_length=32,
unique=True,
verbose_name="Documento de identidad",
),
),
("first_name", models.CharField(max_length=64, verbose_name="Nombre")),
(
"last_name",
models.CharField(max_length=64, verbose_name="Apellidos"),
),
("email", models.EmailField(max_length=254, verbose_name="E-mail")),
("address", models.CharField(max_length=255, verbose_name="Dirección")),
("city", models.CharField(max_length=64, verbose_name="Ciudad")),
("state", models.CharField(max_length=64, verbose_name="Región")),
("country", models.CharField(max_length=64, verbose_name="País")),
("zip", models.CharField(max_length=32, verbose_name="Código postal")),
],
options={
"verbose_name": "Cliente",
"verbose_name_plural": "Clientes",
},
),
migrations.CreateModel( migrations.CreateModel(
name="OrderLine", name="OrderLine",
fields=[ fields=[
@@ -189,6 +226,68 @@ class Migration(migrations.Migration):
verbose_name="Total", verbose_name="Total",
), ),
), ),
(
"billing_address",
models.CharField(
max_length=255, verbose_name="Dirección de facturación"
),
),
(
"billing_city",
models.CharField(
max_length=64, verbose_name="Ciudad de facturación"
),
),
(
"billing_state",
models.CharField(
max_length=64, verbose_name="Región de facturación"
),
),
(
"billing_country",
models.CharField(max_length=64, verbose_name="País de facturación"),
),
(
"billing_zip",
models.CharField(
max_length=32, verbose_name="Código postal de facturación"
),
),
(
"shipping_address",
models.CharField(max_length=255, verbose_name="Dirección"),
),
(
"shipping_city",
models.CharField(max_length=64, verbose_name="Ciudad"),
),
(
"shipping_state",
models.CharField(max_length=64, verbose_name="Región"),
),
(
"shipping_country",
models.CharField(max_length=64, verbose_name="País"),
),
(
"shipping_zip",
models.CharField(max_length=32, verbose_name="Código postal"),
),
(
"contact_phone",
models.CharField(
blank=True, max_length=32, verbose_name="Teléfono de contacto"
),
),
(
"customer",
models.ForeignKey(
on_delete=django.db.models.deletion.PROTECT,
to="shop.customer",
verbose_name="Cliente",
),
),
( (
"lines", "lines",
models.ManyToManyField(to="shop.orderline", verbose_name="Líneas"), models.ManyToManyField(to="shop.orderline", verbose_name="Líneas"),
+69
View File
@@ -80,6 +80,33 @@ class Tax(models.Model):
verbose_name_plural = _("Impuestos") verbose_name_plural = _("Impuestos")
class Customer(models.Model):
vat_id = models.CharField(
max_length=32,
blank=False,
unique=True,
verbose_name=_("Documento de identidad"),
)
first_name = models.CharField(max_length=64, blank=False, verbose_name=_("Nombre"))
last_name = models.CharField(
max_length=64, blank=False, verbose_name=_("Apellidos")
)
email = models.EmailField(blank=False, verbose_name=_("E-mail"))
address = models.CharField(max_length=255, blank=False, verbose_name=_("Dirección"))
city = models.CharField(max_length=64, blank=False, verbose_name=_("Ciudad"))
state = models.CharField(max_length=64, blank=False, verbose_name=_("Región"))
country = models.CharField(max_length=64, blank=False, verbose_name=_("País"))
zip = models.CharField(max_length=32, blank=False, verbose_name=_("Código postal"))
def __str__(self):
return f"{self.vat_id} - {self.first_name} {self.last_name}"
class Meta:
verbose_name = _("Cliente")
verbose_name_plural = _("Clientes")
class OrderLine(models.Model): class OrderLine(models.Model):
product = models.ForeignKey( product = models.ForeignKey(
"shop.Product", "shop.Product",
@@ -154,6 +181,48 @@ class Order(models.Model):
default=Decimal("0"), max_digits=13, decimal_places=2, verbose_name=_("Total") default=Decimal("0"), max_digits=13, decimal_places=2, verbose_name=_("Total")
) )
# Customer reference
customer = models.ForeignKey(
"shop.Customer", on_delete=models.PROTECT, verbose_name=_("Cliente")
)
# Billing information
billing_address = models.CharField(
max_length=255, blank=False, verbose_name=_("Dirección de facturación")
)
billing_city = models.CharField(
max_length=64, blank=False, verbose_name=_("Ciudad de facturación")
)
billing_state = models.CharField(
max_length=64, blank=False, verbose_name=_("Región de facturación")
)
billing_country = models.CharField(
max_length=64, blank=False, verbose_name=_("País de facturación")
)
billing_zip = models.CharField(
max_length=32, blank=False, verbose_name=_("Código postal de facturación")
)
# Shipping information
shipping_address = models.CharField(
max_length=255, blank=False, verbose_name=_("Dirección")
)
shipping_city = models.CharField(
max_length=64, blank=False, verbose_name=_("Ciudad")
)
shipping_state = models.CharField(
max_length=64, blank=False, verbose_name=_("Región")
)
shipping_country = models.CharField(
max_length=64, blank=False, verbose_name=_("País")
)
shipping_zip = models.CharField(
max_length=32, blank=False, verbose_name=_("Código postal")
)
contact_phone = models.CharField(
max_length=32, blank=True, verbose_name=_("Teléfono de contacto")
)
def calculate_total_from_lines(self): def calculate_total_from_lines(self):
self.base_total = self.lines.aggregate(base_total=models.Sum("base_total")).get( self.base_total = self.lines.aggregate(base_total=models.Sum("base_total")).get(
"base_total" "base_total"
+21 -2
View File
@@ -1,6 +1,6 @@
from decimal import Decimal from decimal import Decimal
from rest_framework.test import APITestCase as TestCase from rest_framework.test import APITestCase as TestCase
from shop.models import Product, ProductPrice, OrderLine, Tax from shop.models import Product, ProductPrice, OrderLine, Tax, Customer
from shop.utils import create_order_line_for_product, create_order from shop.utils import create_order_line_for_product, create_order
@@ -34,6 +34,17 @@ class ShopModelsTest(TestCase):
ProductPrice.objects.create(product=self.usb_c, price=Decimal("9.95")) ProductPrice.objects.create(product=self.usb_c, price=Decimal("9.95"))
def test_create_order(self): def test_create_order(self):
self.customer = Customer.objects.create(
first_name='Luke',
last_name='Skywalker',
vat_id='11111111H',
address='Farm Skywalker',
city='Desert',
state='Mos-Eisley',
zip='00001',
country='Tatooine',
)
l1 = create_order_line_for_product( l1 = create_order_line_for_product(
self.potatoes, self.potatoes,
quantity=Decimal("1.5"), quantity=Decimal("1.5"),
@@ -50,7 +61,15 @@ class ShopModelsTest(TestCase):
tax=self.tax, tax=self.tax,
) )
order = create_order(OrderLine.objects.all()) order = create_order(
customer=self.customer,
lines=OrderLine.objects.all(),
billing_address=self.customer.address,
billing_city=self.customer.city,
billing_state=self.customer.state,
billing_zip=self.customer.zip,
billing_country=self.customer.country,
)
assert order.total == l1.total + l2.total + l3.total assert order.total == l1.total + l2.total + l3.total
assert order.base_total == l1.base_total + l2.base_total + l3.base_total assert order.base_total == l1.base_total + l2.base_total + l3.base_total
+47 -4
View File
@@ -1,5 +1,7 @@
from decimal import Decimal from decimal import Decimal
from shop.models import OrderLine, Tax, Product, Order from typing import Iterable
from shop.models import OrderLine, Tax, Product, Order, Customer
from django.db.models import QuerySet from django.db.models import QuerySet
@@ -19,10 +21,51 @@ def create_order_line_for_product(product: Product, quantity: Decimal, tax: Tax)
) )
def create_order(lines: QuerySet): def create_order(
order = Order.objects.create() customer: Customer,
lines: Iterable,
for line in lines.all(): billing_address: str,
billing_city: str,
billing_state: str,
billing_country: str,
billing_zip: str,
shipping_address: str = '',
shipping_city: str = '',
shipping_state: str = '',
shipping_country: str = '',
shipping_zip: str = '',
) -> Order:
billing_address = billing_address if billing_address else customer.address
billing_city = billing_city if billing_city else customer.city
billing_state = billing_state if billing_state else customer.state
billing_country = billing_country if billing_country else customer.country
billing_zip = billing_zip if billing_zip else customer.zip
shipping_address = shipping_address if shipping_address else billing_address
shipping_city = shipping_city if shipping_city else billing_city
shipping_state = shipping_state if shipping_state else billing_state
shipping_country = shipping_country if shipping_country else billing_country
shipping_zip = shipping_zip if shipping_zip else billing_zip
order = Order.objects.create(
customer=customer,
billing_address=billing_address,
billing_city=billing_city,
billing_state=billing_state,
billing_country=billing_country,
billing_zip=billing_zip,
shipping_address=shipping_address,
shipping_city=shipping_city,
shipping_state=shipping_state,
shipping_country=shipping_country,
shipping_zip=shipping_zip,
)
for line in lines:
order.lines.add(line) order.lines.add(line)
order.calculate_total_from_lines() order.calculate_total_from_lines()
+10
View File
@@ -104,6 +104,11 @@ class TestRedsysTPV(APITestCase):
assert response.status_code == status.HTTP_200_OK assert response.status_code == status.HTTP_200_OK
response = self.client.get(
reverse("tpv:ok", kwargs={"order": order.hash})
)
assert response.status_code == status.HTTP_200_OK
def test_redsys_webhook_payment_error(self): def test_redsys_webhook_payment_error(self):
amount_to_pay = Decimal("10.00") amount_to_pay = Decimal("10.00")
order = PaymentTPV.objects.create(amount=amount_to_pay) order = PaymentTPV.objects.create(amount=amount_to_pay)
@@ -167,3 +172,8 @@ class TestRedsysTPV(APITestCase):
order.refresh_from_db() order.refresh_from_db()
assert str(ERROR_CODES.get("0184")) in order.observations assert str(ERROR_CODES.get("0184")) in order.observations
assert order.status == PaymentTPV.StatusChoices.ERROR assert order.status == PaymentTPV.StatusChoices.ERROR
response = self.client.get(
reverse("tpv:ko", kwargs={"order": order.hash})
)
assert response.status_code == status.HTTP_200_OK
+1 -38
View File
@@ -13,10 +13,6 @@ from tpv.signals import redsys_payment_accepted, redsys_payment_rejected
def payment_accepted(request, order): def payment_accepted(request, order):
order = get_object_or_404(PaymentTPV, hash=order) order = get_object_or_404(PaymentTPV, hash=order)
# Ñapa histórica
if request.GET:
return redirect(reverse("tpv:ok", kwargs={"order": order.hash}))
return render( return render(
request, request,
template_name="tpv/order_created.html", template_name="tpv/order_created.html",
@@ -29,13 +25,9 @@ def payment_accepted(request, order):
def payment_rejected(request, order): def payment_rejected(request, order):
order = get_object_or_404(PaymentTPV, hash=order) order = get_object_or_404(PaymentTPV, hash=order)
# Ñapa histórica
if request.GET:
return redirect(reverse("tpv:ko", kwargs={"order": order.hash}))
return render( return render(
request, request,
template_name="tpv/order.html", template_name="tpv/order_created.html",
context={ context={
"order": order, "order": order,
}, },
@@ -67,7 +59,6 @@ def order_created(request, order):
client = RedsysClient() client = RedsysClient()
parameters = client.get_body_for_order(order) parameters = client.get_body_for_order(order)
if request.method == "GET":
return render( return render(
request, request,
template_name="tpv/order_created.html", template_name="tpv/order_created.html",
@@ -81,31 +72,3 @@ def order_created(request, order):
"redsys_target_url": client.get_target_url(), "redsys_target_url": client.get_target_url(),
}, },
) )
elif request.method == "POST":
form = UpdateEmailForm(request.POST)
if form.is_valid():
order.contact_email = form.cleaned_data["contact_email"]
order.save()
return redirect(reverse("tpv:order_created", kwargs={"order": order.hash}))
else:
return render(
request,
template_name="tpv/order_created.html",
context={
"form": UpdateEmailForm({"contact_email": ""}),
"errors": form.errors,
"action": reverse(
"tpv:order_created", kwargs={"order": order.hash}
),
"order": order,
"signature_version": parameters.get("Ds_SignatureVersion"),
"merchant_parameters": parameters.get("Ds_MerchantParameters"),
"signature": parameters.get("Ds_Signature"),
"redsys_target_url": client.get_target_url(),
},
)
return HttpResponse(status=405)