feat: lots of changes
This commit is contained in:
@@ -0,0 +1,36 @@
|
|||||||
|
from typing import Dict
|
||||||
|
|
||||||
|
|
||||||
|
class CRUDPermissionsMixin:
|
||||||
|
view_permission_codes: tuple = ()
|
||||||
|
create_permission_codes: tuple = ()
|
||||||
|
destroy_permission_codes: tuple = ()
|
||||||
|
update_permission_codes: tuple = ()
|
||||||
|
action_permissions: Dict[str, tuple] = {}
|
||||||
|
|
||||||
|
def get_default_action_permissions(self):
|
||||||
|
return {
|
||||||
|
"list": self.view_permission_codes,
|
||||||
|
"retrieve": self.view_permission_codes,
|
||||||
|
"create": self.create_permission_codes,
|
||||||
|
"update": self.update_permission_codes,
|
||||||
|
"partial_update": self.update_permission_codes,
|
||||||
|
"destroy": self.destroy_permission_codes,
|
||||||
|
}
|
||||||
|
|
||||||
|
def has_perm_for_action(self, request, action) -> bool:
|
||||||
|
default_action_permissions = self.get_default_action_permissions()
|
||||||
|
action_permissions = {**default_action_permissions, **self.action_permissions}
|
||||||
|
perms = action_permissions.get(action, ())
|
||||||
|
return request.user.has_perms(perms)
|
||||||
|
|
||||||
|
def has_permission(self, request, view):
|
||||||
|
user = request.user
|
||||||
|
|
||||||
|
if user.is_superuser:
|
||||||
|
return True
|
||||||
|
|
||||||
|
if not user.is_staff:
|
||||||
|
return False
|
||||||
|
|
||||||
|
return self.has_perm_for_action(request=request, action=view.action)
|
||||||
@@ -26,8 +26,10 @@ THIRD_PARTY_APPS = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
PROJECT_APPS = [
|
PROJECT_APPS = [
|
||||||
|
"frontend",
|
||||||
"shop",
|
"shop",
|
||||||
"tpv",
|
"tpv",
|
||||||
|
"users",
|
||||||
]
|
]
|
||||||
|
|
||||||
INSTALLED_APPS = DJANGO_APPS + THIRD_PARTY_APPS + PROJECT_APPS
|
INSTALLED_APPS = DJANGO_APPS + THIRD_PARTY_APPS + PROJECT_APPS
|
||||||
|
|||||||
@@ -7,4 +7,5 @@ urlpatterns = [
|
|||||||
path("api/v1/", include("config.api.v1.urls")),
|
path("api/v1/", include("config.api.v1.urls")),
|
||||||
path("watchman", include("watchman.urls")),
|
path("watchman", include("watchman.urls")),
|
||||||
path("tpv/", include("tpv.urls", namespace="tpv")),
|
path("tpv/", include("tpv.urls", namespace="tpv")),
|
||||||
|
path("auth/", include("users.urls", namespace="users")),
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
from django.contrib import admin
|
||||||
|
|
||||||
|
# Register your models here.
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
|
||||||
|
class FrontendConfig(AppConfig):
|
||||||
|
default_auto_field = "django.db.models.BigAutoField"
|
||||||
|
name = "frontend"
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
from django.db import models
|
||||||
|
|
||||||
|
# Create your models here.
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="es">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>{{ title }}</title>
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<script src="https://cdn.tailwindcss.com"></script>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
tailwind.config = {
|
||||||
|
theme: {
|
||||||
|
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
{% block content %}{% endblock %}
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
from django.test import TestCase
|
||||||
|
|
||||||
|
# Create your tests here.
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
from django.shortcuts import render
|
||||||
|
|
||||||
|
# Create your views here.
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
from rest_framework.permissions import BasePermission
|
||||||
|
from config.api.v1.mixins import CRUDPermissionsMixin
|
||||||
|
|
||||||
|
|
||||||
|
class ProductPermissions(CRUDPermissionsMixin, BasePermission):
|
||||||
|
view_permission_codes = ("shop.view_product",)
|
||||||
|
create_permission_codes = ("shop.add_product",)
|
||||||
|
destroy_permission_codes = ("shop.delete_product",)
|
||||||
|
update_permission_codes = ("shop.change_product",)
|
||||||
|
|
||||||
|
|
||||||
|
class ProductPricePermissions(CRUDPermissionsMixin, BasePermission):
|
||||||
|
view_permission_codes = ("shop.view_productprice",)
|
||||||
|
create_permission_codes = ("shop.add_productprice",)
|
||||||
|
destroy_permission_codes = ("shop.delete_productprice",)
|
||||||
|
update_permission_codes = ("shop.change_productprice",)
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
# Generated by Django 5.0.3 on 2024-03-24 22:50
|
# Generated by Django 5.0.3 on 2024-04-24 14:00
|
||||||
|
|
||||||
import django.db.models.deletion
|
import django.db.models.deletion
|
||||||
import django.utils.timezone
|
import django.utils.timezone
|
||||||
@@ -139,7 +139,12 @@ class Migration(migrations.Migration):
|
|||||||
verbose_name="ID",
|
verbose_name="ID",
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
("uuid", models.UUIDField(default=uuid.uuid4, verbose_name="UUID")),
|
(
|
||||||
|
"uuid",
|
||||||
|
models.UUIDField(
|
||||||
|
db_index=True, default=uuid.uuid4, verbose_name="UUID"
|
||||||
|
),
|
||||||
|
),
|
||||||
(
|
(
|
||||||
"creation_date",
|
"creation_date",
|
||||||
models.DateTimeField(
|
models.DateTimeField(
|
||||||
@@ -346,6 +351,14 @@ class Migration(migrations.Migration):
|
|||||||
verbose_name="Producto",
|
verbose_name="Producto",
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
(
|
||||||
|
"tax",
|
||||||
|
models.ForeignKey(
|
||||||
|
on_delete=django.db.models.deletion.PROTECT,
|
||||||
|
to="shop.tax",
|
||||||
|
verbose_name="Impuesto aplicable",
|
||||||
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
options={
|
options={
|
||||||
"verbose_name": "Precio de producto",
|
"verbose_name": "Precio de producto",
|
||||||
|
|||||||
+29
-12
@@ -7,11 +7,6 @@ from django.utils.text import gettext_lazy as _
|
|||||||
|
|
||||||
|
|
||||||
class Product(models.Model):
|
class Product(models.Model):
|
||||||
class UnitChoices(models.TextChoices):
|
|
||||||
UNIT = "UNIT", _("Unidad")
|
|
||||||
WEIGHT_KG = "KG", _("kg")
|
|
||||||
VOLUME_LITER = "L", _("L")
|
|
||||||
|
|
||||||
name = models.CharField(
|
name = models.CharField(
|
||||||
max_length=96,
|
max_length=96,
|
||||||
blank=False,
|
blank=False,
|
||||||
@@ -30,12 +25,8 @@ class Product(models.Model):
|
|||||||
default=Decimal("0"),
|
default=Decimal("0"),
|
||||||
verbose_name=_("Stock"),
|
verbose_name=_("Stock"),
|
||||||
)
|
)
|
||||||
unit = models.CharField(
|
is_digital_asset = models.BooleanField(default=False)
|
||||||
max_length=6,
|
url = models.URLField(blank=True, verbose_name=_('URL de descarga'))
|
||||||
choices=UnitChoices.choices,
|
|
||||||
default=UnitChoices.UNIT,
|
|
||||||
verbose_name=_("Unidad de medida"),
|
|
||||||
)
|
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.name
|
return self.name
|
||||||
@@ -56,9 +47,19 @@ class ProductPrice(models.Model):
|
|||||||
verbose_name=_("Producto"),
|
verbose_name=_("Producto"),
|
||||||
related_name="prices",
|
related_name="prices",
|
||||||
)
|
)
|
||||||
|
tax = models.ForeignKey(
|
||||||
|
"shop.Tax",
|
||||||
|
on_delete=models.PROTECT,
|
||||||
|
verbose_name=_("Impuesto aplicable"),
|
||||||
|
)
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return f"{self.price}"
|
return f"{self.price} - {self.tax.code}"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def price_with_tax(self):
|
||||||
|
tax_value = self.price * (self.tax.value / 100)
|
||||||
|
return round(self.price + tax_value, 2)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
verbose_name = _("Precio de producto")
|
verbose_name = _("Precio de producto")
|
||||||
@@ -170,7 +171,23 @@ class OrderLine(models.Model):
|
|||||||
|
|
||||||
|
|
||||||
class Order(models.Model):
|
class Order(models.Model):
|
||||||
|
class Statuses(models.TextChoices):
|
||||||
|
STATUS_PENDING = "PEN", _("Pendiente de pago")
|
||||||
|
STATUS_PAID = "PAI", _("Pagado")
|
||||||
|
STATUS_READY = "RDY", _("Preparado")
|
||||||
|
STATUS_TO_BE_SENT = "TBS", _("Listo para ser enviado")
|
||||||
|
STATUS_SENT = "SNT", _("Enviado")
|
||||||
|
STATUS_DELIVERED = "DLV", _("Entregado")
|
||||||
|
STATUS_FINISHED = "FIN", _("Finalizado")
|
||||||
|
STATUS_CANCELED = "CAN", _("Cancelado")
|
||||||
|
STATUS_RETURNED = "RTN", _("Devuelto")
|
||||||
|
|
||||||
uuid = models.UUIDField(default=uuid4, verbose_name=_("UUID"), db_index=True)
|
uuid = models.UUIDField(default=uuid4, verbose_name=_("UUID"), db_index=True)
|
||||||
|
|
||||||
|
status = models.CharField(
|
||||||
|
max_length=3, default=Statuses.STATUS_PENDING, verbose_name=_("Estado")
|
||||||
|
)
|
||||||
|
|
||||||
creation_date = models.DateTimeField(
|
creation_date = models.DateTimeField(
|
||||||
auto_now_add=True, verbose_name=_("Fecha de creación")
|
auto_now_add=True, verbose_name=_("Fecha de creación")
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -29,9 +29,15 @@ class ShopModelsTest(TestCase):
|
|||||||
unit=Product.UnitChoices.UNIT,
|
unit=Product.UnitChoices.UNIT,
|
||||||
)
|
)
|
||||||
|
|
||||||
ProductPrice.objects.create(product=self.potatoes, price=Decimal("0.80"))
|
ProductPrice.objects.create(
|
||||||
ProductPrice.objects.create(product=self.gasoline, price=Decimal("1.15"))
|
product=self.potatoes, price=Decimal("0.80"), tax=self.tax
|
||||||
ProductPrice.objects.create(product=self.usb_c, price=Decimal("9.95"))
|
)
|
||||||
|
ProductPrice.objects.create(
|
||||||
|
product=self.gasoline, price=Decimal("1.15"), tax=self.tax
|
||||||
|
)
|
||||||
|
ProductPrice.objects.create(
|
||||||
|
product=self.usb_c, price=Decimal("9.95"), tax=self.tax
|
||||||
|
)
|
||||||
|
|
||||||
def test_create_order(self):
|
def test_create_order(self):
|
||||||
self.customer = Customer.objects.create(
|
self.customer = Customer.objects.create(
|
||||||
@@ -57,19 +63,16 @@ class ShopModelsTest(TestCase):
|
|||||||
l1 = create_order_line_for_product(
|
l1 = create_order_line_for_product(
|
||||||
self.potatoes,
|
self.potatoes,
|
||||||
quantity=Decimal("1.5"),
|
quantity=Decimal("1.5"),
|
||||||
tax=self.tax,
|
|
||||||
order=order,
|
order=order,
|
||||||
)
|
)
|
||||||
l2 = create_order_line_for_product(
|
l2 = create_order_line_for_product(
|
||||||
self.gasoline,
|
self.gasoline,
|
||||||
quantity=Decimal("40"),
|
quantity=Decimal("40"),
|
||||||
tax=self.tax,
|
|
||||||
order=order,
|
order=order,
|
||||||
)
|
)
|
||||||
l3 = create_order_line_for_product(
|
l3 = create_order_line_for_product(
|
||||||
self.usb_c,
|
self.usb_c,
|
||||||
quantity=Decimal("1.00"),
|
quantity=Decimal("1.00"),
|
||||||
tax=self.tax,
|
|
||||||
order=order,
|
order=order,
|
||||||
)
|
)
|
||||||
order.calculate_total_from_lines()
|
order.calculate_total_from_lines()
|
||||||
|
|||||||
+7
-8
@@ -5,20 +5,19 @@ from shop.models import OrderLine, Tax, Product, Order, Customer
|
|||||||
from django.db.models import QuerySet
|
from django.db.models import QuerySet
|
||||||
|
|
||||||
|
|
||||||
def create_order_line_for_product(
|
def create_order_line_for_product(product: Product, quantity: Decimal, order: Order):
|
||||||
product: Product, quantity: Decimal, tax: Tax, order: Order
|
price = product.prices.last()
|
||||||
):
|
base_total = round(price.price * quantity, 2)
|
||||||
price = product.prices.last().price
|
tax_value = price.tax.value / Decimal("100")
|
||||||
base_total = round(price * quantity, 2)
|
taxes = round(base_total * tax_value, 2)
|
||||||
taxes = round(base_total * (tax.value / Decimal("100")), 2)
|
|
||||||
|
|
||||||
return OrderLine.objects.create(
|
return OrderLine.objects.create(
|
||||||
order=order,
|
order=order,
|
||||||
product=product,
|
product=product,
|
||||||
quantity=quantity,
|
quantity=quantity,
|
||||||
price=price,
|
price=price.price,
|
||||||
base_total=base_total,
|
base_total=base_total,
|
||||||
tax_value=tax.value,
|
tax_value=tax_value,
|
||||||
taxes=taxes,
|
taxes=taxes,
|
||||||
total=base_total + taxes,
|
total=base_total + taxes,
|
||||||
)
|
)
|
||||||
|
|||||||
+126
-24
@@ -1,39 +1,36 @@
|
|||||||
import base64
|
import base64
|
||||||
import json
|
import json
|
||||||
|
import requests
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
|
|
||||||
from tpv.models import PaymentTransaction
|
from tpv.models import PaymentTransaction
|
||||||
from tpv.utils import compute_signature
|
from tpv.utils import compute_signature, decode_b64_dict
|
||||||
|
from tpv.settings import TransactionTypes
|
||||||
|
|
||||||
|
from tpv.exceptions import RedsysPaymentException
|
||||||
class TransactionTypes:
|
|
||||||
AUTHORIZATION = 0
|
|
||||||
PREAUTHORIZATION = 1
|
|
||||||
REPLACEMENT_PREAUTHORIZATION = 11
|
|
||||||
CONFIRMATION = 2
|
|
||||||
REFUND = 3
|
|
||||||
SEPARATED_CONFIRMATION = 8
|
|
||||||
CANCELED = 9
|
|
||||||
PAYGOLD = 15
|
|
||||||
PUCE_AUTHENTICATION = 17
|
|
||||||
REFUND_WITHOUT_ORIGINAL = 34
|
|
||||||
BET_PRICE = 37
|
|
||||||
PAYMENT_CANCELED = 45
|
|
||||||
REFUND_CANCELED = 46
|
|
||||||
SEPARATED_CONFIRMATION_CANCELED = 47
|
|
||||||
LINK_EXPIRATION_CHANGED_FOR_PAYGOLD = 51
|
|
||||||
|
|
||||||
|
|
||||||
class RedsysClient:
|
class RedsysClient:
|
||||||
DEBUG_ENVIRONMENT_URL = "https://sis-t.redsys.es:25443/sis/realizarPago"
|
DEBUG_ENVIRONMENT_URL = "https://sis-t.redsys.es:25443/sis/realizarPago"
|
||||||
PROD_ENVIRONMENT_URL = ""
|
PROD_ENVIRONMENT_URL = ""
|
||||||
|
|
||||||
|
REST_DEBUG_ENVIRONMENT_URL = (
|
||||||
|
"https://sis-t.redsys.es:25443/sis/rest/trataPeticionREST"
|
||||||
|
)
|
||||||
|
REST_PROD_ENVIRONMENT_URL = ""
|
||||||
|
|
||||||
def get_target_url(self):
|
def get_target_url(self):
|
||||||
if settings.DEBUG:
|
if settings.DEBUG:
|
||||||
return self.DEBUG_ENVIRONMENT_URL
|
return self.DEBUG_ENVIRONMENT_URL
|
||||||
return self.PROD_ENVIRONMENT_URL
|
return self.PROD_ENVIRONMENT_URL
|
||||||
|
|
||||||
|
def get_rest_target_url(self):
|
||||||
|
if settings.DEBUG:
|
||||||
|
return self.REST_DEBUG_ENVIRONMENT_URL
|
||||||
|
return self.REST_PROD_ENVIRONMENT_URL
|
||||||
|
|
||||||
def get_merchant_code(self) -> str:
|
def get_merchant_code(self) -> str:
|
||||||
return settings.REDSYS_MERCHANT_CODE
|
return settings.REDSYS_MERCHANT_CODE
|
||||||
|
|
||||||
@@ -66,7 +63,7 @@ class RedsysClient:
|
|||||||
key = self.get_shared_secret()
|
key = self.get_shared_secret()
|
||||||
return compute_signature(str(hash), payload, key).decode()
|
return compute_signature(str(hash), payload, key).decode()
|
||||||
|
|
||||||
def get_merchant_parameters_for_transaction(
|
def _get_merchant_parameters_for_transaction(
|
||||||
self,
|
self,
|
||||||
transaction: PaymentTransaction,
|
transaction: PaymentTransaction,
|
||||||
transaction_type: int = TransactionTypes.AUTHORIZATION,
|
transaction_type: int = TransactionTypes.AUTHORIZATION,
|
||||||
@@ -90,23 +87,22 @@ class RedsysClient:
|
|||||||
"DS_MERCHANT_URLOK": self.get_merchant_url_ok_for_transaction(transaction),
|
"DS_MERCHANT_URLOK": self.get_merchant_url_ok_for_transaction(transaction),
|
||||||
}
|
}
|
||||||
|
|
||||||
def get_encoded_merchant_parameters_for_transaction(
|
def _get_encoded_merchant_parameters_for_transaction(
|
||||||
self,
|
self,
|
||||||
transaction: PaymentTransaction,
|
transaction: PaymentTransaction,
|
||||||
transaction_type: int = TransactionTypes.AUTHORIZATION,
|
transaction_type: int = TransactionTypes.AUTHORIZATION,
|
||||||
) -> str:
|
) -> str:
|
||||||
body = self.get_merchant_parameters_for_transaction(
|
body = self._get_merchant_parameters_for_transaction(
|
||||||
transaction, transaction_type
|
transaction, transaction_type
|
||||||
)
|
)
|
||||||
stringified_body = json.dumps(body)
|
return self._encode_body(body)
|
||||||
return base64.b64encode(stringified_body.encode()).decode("utf-8")
|
|
||||||
|
|
||||||
def get_body_for_transaction(
|
def get_body_for_transaction(
|
||||||
self,
|
self,
|
||||||
transaction: PaymentTransaction,
|
transaction: PaymentTransaction,
|
||||||
transaction_type: int = TransactionTypes.AUTHORIZATION,
|
transaction_type: int = TransactionTypes.AUTHORIZATION,
|
||||||
) -> dict:
|
) -> dict:
|
||||||
merchant_parameters = self.get_encoded_merchant_parameters_for_transaction(
|
merchant_parameters = self._get_encoded_merchant_parameters_for_transaction(
|
||||||
transaction, transaction_type
|
transaction, transaction_type
|
||||||
)
|
)
|
||||||
signature = self.get_signature(transaction.hash.hex, merchant_parameters)
|
signature = self.get_signature(transaction.hash.hex, merchant_parameters)
|
||||||
@@ -116,3 +112,109 @@ class RedsysClient:
|
|||||||
"Ds_SignatureVersion": "HMAC_SHA256_V1",
|
"Ds_SignatureVersion": "HMAC_SHA256_V1",
|
||||||
"Ds_Signature": signature,
|
"Ds_Signature": signature,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def _encode_body(self, value):
|
||||||
|
stringified_body = json.dumps(value)
|
||||||
|
return base64.b64encode(stringified_body.encode()).decode("utf-8")
|
||||||
|
|
||||||
|
def _get_rest_merchant_parameters_for_transaction(
|
||||||
|
self,
|
||||||
|
transaction: PaymentTransaction,
|
||||||
|
pan: str,
|
||||||
|
expiry_date: str,
|
||||||
|
cvv2: str,
|
||||||
|
transaction_type: int = TransactionTypes.AUTHORIZATION,
|
||||||
|
) -> dict:
|
||||||
|
merchant_code = self.get_merchant_code()
|
||||||
|
|
||||||
|
return {
|
||||||
|
"DS_MERCHANT_AMOUNT": str(transaction.amount_integer),
|
||||||
|
"DS_MERCHANT_CURRENCY": self.get_currency_code(),
|
||||||
|
"DS_MERCHANT_CVV2": cvv2,
|
||||||
|
"DS_MERCHANT_EXPIRYDATE": expiry_date,
|
||||||
|
"DS_MERCHANT_MERCHANTCODE": merchant_code,
|
||||||
|
"DS_MERCHANT_ORDER": transaction.hash.hex,
|
||||||
|
"DS_MERCHANT_PAN": pan,
|
||||||
|
"DS_MERCHANT_TERMINAL": "1",
|
||||||
|
"DS_MERCHANT_TRANSACTIONTYPE": transaction_type,
|
||||||
|
}
|
||||||
|
|
||||||
|
def _get_rest_encoded_merchant_parameters_for_transaction(
|
||||||
|
self,
|
||||||
|
transaction: PaymentTransaction,
|
||||||
|
pan: str,
|
||||||
|
expiry_date: str,
|
||||||
|
cvv2: str,
|
||||||
|
transaction_type: int = TransactionTypes.AUTHORIZATION,
|
||||||
|
) -> str:
|
||||||
|
body = self._get_rest_merchant_parameters_for_transaction(
|
||||||
|
transaction,
|
||||||
|
pan=pan,
|
||||||
|
expiry_date=expiry_date,
|
||||||
|
cvv2=cvv2,
|
||||||
|
transaction_type=transaction_type,
|
||||||
|
)
|
||||||
|
return self._encode_body(body)
|
||||||
|
|
||||||
|
def _get_rest_body_for_transaction(
|
||||||
|
self,
|
||||||
|
transaction: PaymentTransaction,
|
||||||
|
pan: str,
|
||||||
|
expiry_date: str,
|
||||||
|
cvv2: str,
|
||||||
|
transaction_type: int = TransactionTypes.AUTHORIZATION,
|
||||||
|
) -> dict:
|
||||||
|
merchant_parameters = (
|
||||||
|
self._get_rest_encoded_merchant_parameters_for_transaction(
|
||||||
|
transaction=transaction,
|
||||||
|
pan=pan,
|
||||||
|
expiry_date=expiry_date,
|
||||||
|
cvv2=cvv2,
|
||||||
|
transaction_type=transaction_type,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
signature = self.get_signature(transaction.hash.hex, merchant_parameters)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"Ds_MerchantParameters": merchant_parameters,
|
||||||
|
"Ds_SignatureVersion": "HMAC_SHA256_V1",
|
||||||
|
"Ds_Signature": signature,
|
||||||
|
}
|
||||||
|
|
||||||
|
def make_request_for_transaction(
|
||||||
|
self,
|
||||||
|
transaction: PaymentTransaction,
|
||||||
|
pan: str,
|
||||||
|
expiry_date: str,
|
||||||
|
cvv2: str,
|
||||||
|
transaction_type: int = TransactionTypes.AUTHORIZATION,
|
||||||
|
):
|
||||||
|
body = self._get_rest_body_for_transaction(
|
||||||
|
transaction=transaction,
|
||||||
|
pan=pan,
|
||||||
|
expiry_date=expiry_date,
|
||||||
|
cvv2=cvv2,
|
||||||
|
transaction_type=transaction_type,
|
||||||
|
)
|
||||||
|
url = self.get_rest_target_url()
|
||||||
|
response = requests.post(url, body)
|
||||||
|
return response
|
||||||
|
|
||||||
|
def pay_transaction_rest(
|
||||||
|
self, transaction: PaymentTransaction, pan: str, expiry_date: str, cvv2: str
|
||||||
|
):
|
||||||
|
response = self.make_request_for_transaction(
|
||||||
|
transaction, pan, expiry_date, cvv2
|
||||||
|
)
|
||||||
|
data = response.json()
|
||||||
|
error_code: str = data.get("errorCode", "")
|
||||||
|
|
||||||
|
if error_code:
|
||||||
|
error_code.replace("SIS0", "0")
|
||||||
|
|
||||||
|
raise RedsysPaymentException(error_code)
|
||||||
|
|
||||||
|
merchant_parameters = data.get("Ds_MerchantParameters")
|
||||||
|
parameters = decode_b64_dict(merchant_parameters)
|
||||||
|
|
||||||
|
return parameters
|
||||||
|
|||||||
@@ -1,6 +1,24 @@
|
|||||||
from django.utils.text import gettext_lazy as _
|
from django.utils.text import gettext_lazy as _
|
||||||
|
|
||||||
|
|
||||||
|
class TransactionTypes:
|
||||||
|
AUTHORIZATION = 0
|
||||||
|
PREAUTHORIZATION = 1
|
||||||
|
REPLACEMENT_PREAUTHORIZATION = 11
|
||||||
|
CONFIRMATION = 2
|
||||||
|
REFUND = 3
|
||||||
|
SEPARATED_CONFIRMATION = 8
|
||||||
|
CANCELED = 9
|
||||||
|
PAYGOLD = 15
|
||||||
|
PUCE_AUTHENTICATION = 17
|
||||||
|
REFUND_WITHOUT_ORIGINAL = 34
|
||||||
|
BET_PRICE = 37
|
||||||
|
PAYMENT_CANCELED = 45
|
||||||
|
REFUND_CANCELED = 46
|
||||||
|
SEPARATED_CONFIRMATION_CANCELED = 47
|
||||||
|
LINK_EXPIRATION_CHANGED_FOR_PAYGOLD = 51
|
||||||
|
|
||||||
|
|
||||||
CURRENCY_CODES = {
|
CURRENCY_CODES = {
|
||||||
8: "LEK ALL",
|
8: "LEK ALL",
|
||||||
12: "ALGERIAN DINAR DZD",
|
12: "ALGERIAN DINAR DZD",
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ class TestRedsysTPV(APITestCase):
|
|||||||
transaction.save()
|
transaction.save()
|
||||||
|
|
||||||
client = RedsysClient()
|
client = RedsysClient()
|
||||||
merchant_parameters = client.get_merchant_parameters_for_transaction(
|
merchant_parameters = client._get_merchant_parameters_for_transaction(
|
||||||
transaction
|
transaction
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
+38
-5
@@ -8,10 +8,12 @@ import re
|
|||||||
|
|
||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
from django.utils.text import gettext_lazy as _
|
from django.utils.text import gettext_lazy as _
|
||||||
|
from django.utils import timezone
|
||||||
|
|
||||||
from tpv.exceptions import RedsysValidationException, RedsysPaymentException
|
from tpv.exceptions import RedsysValidationException, RedsysPaymentException
|
||||||
from tpv.models import PaymentTransaction
|
from tpv.models import PaymentTransaction
|
||||||
from tpv.settings import ERROR_CODES
|
from tpv.settings import ERROR_CODES
|
||||||
|
from tpv.signals import redsys_payment_accepted
|
||||||
|
|
||||||
|
|
||||||
def compute_signature(salt, payload, key):
|
def compute_signature(salt, payload, key):
|
||||||
@@ -64,10 +66,6 @@ def validate_payment_for_transaction(
|
|||||||
'Ds_ECI': '05',
|
'Ds_ECI': '05',
|
||||||
'Ds_Response_Description': 'OPERACION AUTORIZADA'
|
'Ds_Response_Description': 'OPERACION AUTORIZADA'
|
||||||
}
|
}
|
||||||
|
|
||||||
:param request:
|
|
||||||
:param transaction:
|
|
||||||
:return:
|
|
||||||
"""
|
"""
|
||||||
data = request.POST
|
data = request.POST
|
||||||
|
|
||||||
@@ -78,7 +76,7 @@ def validate_payment_for_transaction(
|
|||||||
_("No se ha recibido ningún valor para Ds_MerchantParameters")
|
_("No se ha recibido ningún valor para Ds_MerchantParameters")
|
||||||
)
|
)
|
||||||
|
|
||||||
merchant_params = base64.b64decode(merchant_parameters).decode()
|
merchant_params = decode_b64_string(merchant_parameters)
|
||||||
result = json.loads(merchant_params)
|
result = json.loads(merchant_params)
|
||||||
|
|
||||||
transaction_hex = result.get("Ds_Order")
|
transaction_hex = result.get("Ds_Order")
|
||||||
@@ -96,6 +94,41 @@ def validate_payment_for_transaction(
|
|||||||
return amount
|
return amount
|
||||||
|
|
||||||
|
|
||||||
|
def validate_expiry_date(expiry_date: str):
|
||||||
|
if len(expiry_date) != 4:
|
||||||
|
return False
|
||||||
|
|
||||||
|
month, year = int(expiry_date[2:4]), int(expiry_date[:2])
|
||||||
|
|
||||||
|
if month < 1 or month > 12:
|
||||||
|
return False
|
||||||
|
|
||||||
|
current_year = int(str(timezone.now().year)[2:])
|
||||||
|
current_month = timezone.now().month
|
||||||
|
|
||||||
|
if year < current_year:
|
||||||
|
return False
|
||||||
|
|
||||||
|
if year == current_year and month > current_month:
|
||||||
|
return False
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
def pay_transaction(transaction: PaymentTransaction, amount_paid: Decimal):
|
def pay_transaction(transaction: PaymentTransaction, amount_paid: Decimal):
|
||||||
transaction.status = PaymentTransaction.StatusChoices.PAID
|
transaction.status = PaymentTransaction.StatusChoices.PAID
|
||||||
transaction.save()
|
transaction.save()
|
||||||
|
|
||||||
|
redsys_payment_accepted.send_robust(
|
||||||
|
PaymentTransaction.__class__,
|
||||||
|
hash=transaction.hash,
|
||||||
|
amount=amount_paid,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def decode_b64_string(value: str):
|
||||||
|
return base64.b64decode(value).decode()
|
||||||
|
|
||||||
|
|
||||||
|
def decode_b64_dict(value: str):
|
||||||
|
return json.loads(decode_b64_string(value))
|
||||||
|
|||||||
@@ -41,9 +41,6 @@ def webhook(request, transaction):
|
|||||||
try:
|
try:
|
||||||
amount_paid = validate_payment_for_transaction(request, transaction)
|
amount_paid = validate_payment_for_transaction(request, transaction)
|
||||||
pay_transaction(transaction, amount_paid)
|
pay_transaction(transaction, amount_paid)
|
||||||
redsys_payment_accepted.send_robust(
|
|
||||||
PaymentTransaction.__class__, hash=transaction.hash
|
|
||||||
)
|
|
||||||
|
|
||||||
return HttpResponse(status=200)
|
return HttpResponse(status=200)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
from django.contrib import admin
|
||||||
|
|
||||||
|
# Register your models here.
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
|
||||||
|
class UsersConfig(AppConfig):
|
||||||
|
default_auto_field = "django.db.models.BigAutoField"
|
||||||
|
name = "users"
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
from django.db import models
|
||||||
|
|
||||||
|
# Create your models here.
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
{% extends 'base/base.html' %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
|
||||||
|
<section class="bg-gray-50 dark:bg-gray-900">
|
||||||
|
<div class="flex flex-col items-center justify-center px-6 py-8 mx-auto md:h-screen lg:py-0">
|
||||||
|
<a href="#" class="flex items-center mb-6 text-2xl font-semibold text-gray-900 dark:text-white">
|
||||||
|
<img class="w-8 h-8 mr-2" src="https://flowbite.s3.amazonaws.com/blocks/marketing-ui/logo.svg" alt="logo">
|
||||||
|
Flowbite
|
||||||
|
</a>
|
||||||
|
<div class="w-full bg-white rounded-lg shadow dark:border md:mt-0 sm:max-w-md xl:p-0 dark:bg-gray-800 dark:border-gray-700">
|
||||||
|
<div class="p-6 space-y-4 md:space-y-6 sm:p-8">
|
||||||
|
<h1 class="text-xl font-bold leading-tight tracking-tight text-gray-900 md:text-2xl dark:text-white">
|
||||||
|
Sign in to your account
|
||||||
|
</h1>
|
||||||
|
<form class="space-y-4 md:space-y-6" action="#">
|
||||||
|
<div>
|
||||||
|
<label for="email" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Your email</label>
|
||||||
|
<input type="email" name="email" id="email" class="bg-gray-50 border border-gray-300 text-gray-900 sm:text-sm rounded-lg focus:ring-primary-600 focus:border-primary-600 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" placeholder="name@company.com" required="">
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="password" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Password</label>
|
||||||
|
<input type="password" name="password" id="password" placeholder="••••••••" class="bg-gray-50 border border-gray-300 text-gray-900 sm:text-sm rounded-lg focus:ring-primary-600 focus:border-primary-600 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" required="">
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<div class="flex items-start">
|
||||||
|
<div class="flex items-center h-5">
|
||||||
|
<input id="remember" aria-describedby="remember" type="checkbox" class="w-4 h-4 border border-gray-300 rounded bg-gray-50 focus:ring-3 focus:ring-primary-300 dark:bg-gray-700 dark:border-gray-600 dark:focus:ring-primary-600 dark:ring-offset-gray-800" required="">
|
||||||
|
</div>
|
||||||
|
<div class="ml-3 text-sm">
|
||||||
|
<label for="remember" class="text-gray-500 dark:text-gray-300">Remember me</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<a href="#" class="text-sm font-medium text-primary-600 hover:underline dark:text-primary-500">Forgot password?</a>
|
||||||
|
</div>
|
||||||
|
<button type="submit" class="w-full text-white bg-primary-600 hover:bg-primary-700 focus:ring-4 focus:outline-none focus:ring-primary-300 font-medium rounded-lg text-sm px-5 py-2.5 text-center dark:bg-primary-600 dark:hover:bg-primary-700 dark:focus:ring-primary-800">Sign in</button>
|
||||||
|
<p class="text-sm font-light text-gray-500 dark:text-gray-400">
|
||||||
|
Don’t have an account yet? <a href="#" class="font-medium text-primary-600 hover:underline dark:text-primary-500">Sign up</a>
|
||||||
|
</p>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
{% endblock %}
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
{% extends 'base/base.html' %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<section class="bg-gray-50 dark:bg-gray-900">
|
||||||
|
<div class="flex flex-col items-center justify-center px-6 py-8 mx-auto md:h-screen lg:py-0">
|
||||||
|
<a href="#" class="flex items-center mb-6 text-2xl font-semibold text-gray-900 dark:text-white">
|
||||||
|
<img class="w-8 h-8 mr-2" src="https://flowbite.s3.amazonaws.com/blocks/marketing-ui/logo.svg" alt="logo">
|
||||||
|
Flowbite
|
||||||
|
</a>
|
||||||
|
<div class="w-full bg-white rounded-lg shadow dark:border md:mt-0 sm:max-w-md xl:p-0 dark:bg-gray-800 dark:border-gray-700">
|
||||||
|
<div class="p-6 space-y-4 md:space-y-6 sm:p-8">
|
||||||
|
<h1 class="text-xl font-bold leading-tight tracking-tight text-gray-900 md:text-2xl dark:text-white">
|
||||||
|
Create and account
|
||||||
|
</h1>
|
||||||
|
<form class="space-y-4 md:space-y-6" action="#">
|
||||||
|
<div>
|
||||||
|
<label for="email" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Your email</label>
|
||||||
|
<input type="email" name="email" id="email" class="bg-gray-50 border border-gray-300 text-gray-900 sm:text-sm rounded-lg focus:ring-primary-600 focus:border-primary-600 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" placeholder="name@company.com" required="">
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="password" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Password</label>
|
||||||
|
<input type="password" name="password" id="password" placeholder="••••••••" class="bg-gray-50 border border-gray-300 text-gray-900 sm:text-sm rounded-lg focus:ring-primary-600 focus:border-primary-600 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" required="">
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="confirm-password" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Confirm password</label>
|
||||||
|
<input type="confirm-password" name="confirm-password" id="confirm-password" placeholder="••••••••" class="bg-gray-50 border border-gray-300 text-gray-900 sm:text-sm rounded-lg focus:ring-primary-600 focus:border-primary-600 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" required="">
|
||||||
|
</div>
|
||||||
|
<div class="flex items-start">
|
||||||
|
<div class="flex items-center h-5">
|
||||||
|
<input id="terms" aria-describedby="terms" type="checkbox" class="w-4 h-4 border border-gray-300 rounded bg-gray-50 focus:ring-3 focus:ring-primary-300 dark:bg-gray-700 dark:border-gray-600 dark:focus:ring-primary-600 dark:ring-offset-gray-800" required="">
|
||||||
|
</div>
|
||||||
|
<div class="ml-3 text-sm">
|
||||||
|
<label for="terms" class="font-light text-gray-500 dark:text-gray-300">I accept the <a class="font-medium text-primary-600 hover:underline dark:text-primary-500" href="#">Terms and Conditions</a></label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button type="submit" class="w-full text-white bg-primary-600 hover:bg-primary-700 focus:ring-4 focus:outline-none focus:ring-primary-300 font-medium rounded-lg text-sm px-5 py-2.5 text-center dark:bg-primary-600 dark:hover:bg-primary-700 dark:focus:ring-primary-800">Create an account</button>
|
||||||
|
<p class="text-sm font-light text-gray-500 dark:text-gray-400">
|
||||||
|
Already have an account? <a href="#" class="font-medium text-primary-600 hover:underline dark:text-primary-500">Login here</a>
|
||||||
|
</p>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
{% endblock %}
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
{% extends 'base/base.html' %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
|
||||||
|
<section class="bg-gray-50 dark:bg-gray-900">
|
||||||
|
<div class="flex flex-col items-center justify-center px-6 py-8 mx-auto md:h-screen lg:py-0">
|
||||||
|
<a href="#" class="flex items-center mb-6 text-2xl font-semibold text-gray-900 dark:text-white">
|
||||||
|
<img class="w-8 h-8 mr-2" src="https://flowbite.s3.amazonaws.com/blocks/marketing-ui/logo.svg" alt="logo">
|
||||||
|
Shoppy
|
||||||
|
</a>
|
||||||
|
<div class="w-full p-6 bg-white rounded-lg shadow dark:border md:mt-0 sm:max-w-md dark:bg-gray-800 dark:border-gray-700 sm:p-8">
|
||||||
|
<h2 class="mb-1 text-xl font-bold leading-tight tracking-tight text-gray-900 md:text-2xl dark:text-white">
|
||||||
|
Change Password
|
||||||
|
</h2>
|
||||||
|
<form class="mt-4 space-y-4 lg:mt-5 md:space-y-5" action="#">
|
||||||
|
<div>
|
||||||
|
<label for="email" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Your email</label>
|
||||||
|
<input type="email" name="email" id="email" class="bg-gray-50 border border-gray-300 text-gray-900 sm:text-sm rounded-lg focus:ring-primary-600 focus:border-primary-600 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" placeholder="name@company.com" required="">
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="password" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">New Password</label>
|
||||||
|
<input type="password" name="password" id="password" placeholder="••••••••" class="bg-gray-50 border border-gray-300 text-gray-900 sm:text-sm rounded-lg focus:ring-primary-600 focus:border-primary-600 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" required="">
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="confirm-password" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Confirm password</label>
|
||||||
|
<input type="confirm-password" name="confirm-password" id="confirm-password" placeholder="••••••••" class="bg-gray-50 border border-gray-300 text-gray-900 sm:text-sm rounded-lg focus:ring-primary-600 focus:border-primary-600 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" required="">
|
||||||
|
</div>
|
||||||
|
<div class="flex items-start">
|
||||||
|
<div class="flex items-center h-5">
|
||||||
|
<input id="newsletter" aria-describedby="newsletter" type="checkbox" class="w-4 h-4 border border-gray-300 rounded bg-gray-50 focus:ring-3 focus:ring-primary-300 dark:bg-gray-700 dark:border-gray-600 dark:focus:ring-primary-600 dark:ring-offset-gray-800" required="">
|
||||||
|
</div>
|
||||||
|
<div class="ml-3 text-sm">
|
||||||
|
<label for="newsletter" class="font-light text-gray-500 dark:text-gray-300">I accept the <a class="font-medium text-primary-600 hover:underline dark:text-primary-500" href="#">Terms and Conditions</a></label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button type="submit" class="w-full text-white bg-primary-600 hover:bg-primary-700 focus:ring-4 focus:outline-none focus:ring-primary-300 font-medium rounded-lg text-sm px-5 py-2.5 text-center dark:bg-primary-600 dark:hover:bg-primary-700 dark:focus:ring-primary-800">Reset passwod</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{% endblock %}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
from django.test import TestCase
|
||||||
|
|
||||||
|
# Create your tests here.
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
from django.urls import path
|
||||||
|
from users.views import login, register, reset_password
|
||||||
|
|
||||||
|
|
||||||
|
app_name = "users"
|
||||||
|
|
||||||
|
|
||||||
|
urlpatterns = [
|
||||||
|
path("login", login, name="login"),
|
||||||
|
path("sign-up", register, name="register"),
|
||||||
|
path("reset-password", reset_password, name="reset_password"),
|
||||||
|
]
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
from django.shortcuts import render
|
||||||
|
|
||||||
|
|
||||||
|
def login(request):
|
||||||
|
return render(request, "users/login.html", {})
|
||||||
|
|
||||||
|
|
||||||
|
def register(request):
|
||||||
|
return render(request, "users/register.html", {})
|
||||||
|
|
||||||
|
|
||||||
|
def reset_password(request):
|
||||||
|
return render(request, "users/reset_password.html", {})
|
||||||
Reference in New Issue
Block a user