diff --git a/config/api/v1/mixins.py b/config/api/v1/mixins.py
new file mode 100644
index 0000000..533c4b3
--- /dev/null
+++ b/config/api/v1/mixins.py
@@ -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)
diff --git a/config/settings/base.py b/config/settings/base.py
index f378380..231f688 100644
--- a/config/settings/base.py
+++ b/config/settings/base.py
@@ -26,8 +26,10 @@ THIRD_PARTY_APPS = [
]
PROJECT_APPS = [
+ "frontend",
"shop",
"tpv",
+ "users",
]
INSTALLED_APPS = DJANGO_APPS + THIRD_PARTY_APPS + PROJECT_APPS
diff --git a/config/urls.py b/config/urls.py
index a72fc89..5334ecb 100644
--- a/config/urls.py
+++ b/config/urls.py
@@ -7,4 +7,5 @@ urlpatterns = [
path("api/v1/", include("config.api.v1.urls")),
path("watchman", include("watchman.urls")),
path("tpv/", include("tpv.urls", namespace="tpv")),
+ path("auth/", include("users.urls", namespace="users")),
]
diff --git a/frontend/__init__.py b/frontend/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/frontend/admin.py b/frontend/admin.py
new file mode 100644
index 0000000..8c38f3f
--- /dev/null
+++ b/frontend/admin.py
@@ -0,0 +1,3 @@
+from django.contrib import admin
+
+# Register your models here.
diff --git a/frontend/apps.py b/frontend/apps.py
new file mode 100644
index 0000000..c626efa
--- /dev/null
+++ b/frontend/apps.py
@@ -0,0 +1,6 @@
+from django.apps import AppConfig
+
+
+class FrontendConfig(AppConfig):
+ default_auto_field = "django.db.models.BigAutoField"
+ name = "frontend"
diff --git a/frontend/migrations/__init__.py b/frontend/migrations/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/frontend/models.py b/frontend/models.py
new file mode 100644
index 0000000..71a8362
--- /dev/null
+++ b/frontend/models.py
@@ -0,0 +1,3 @@
+from django.db import models
+
+# Create your models here.
diff --git a/frontend/templates/base/base.html b/frontend/templates/base/base.html
new file mode 100644
index 0000000..69aaf88
--- /dev/null
+++ b/frontend/templates/base/base.html
@@ -0,0 +1,21 @@
+
+
+
+
+ {{ title }}
+
+
+
+
+
+
+
+ {% block content %}{% endblock %}
+
+
diff --git a/frontend/tests.py b/frontend/tests.py
new file mode 100644
index 0000000..7ce503c
--- /dev/null
+++ b/frontend/tests.py
@@ -0,0 +1,3 @@
+from django.test import TestCase
+
+# Create your tests here.
diff --git a/frontend/views.py b/frontend/views.py
new file mode 100644
index 0000000..91ea44a
--- /dev/null
+++ b/frontend/views.py
@@ -0,0 +1,3 @@
+from django.shortcuts import render
+
+# Create your views here.
diff --git a/shop/api/v1/permissions.py b/shop/api/v1/permissions.py
new file mode 100644
index 0000000..91a7a03
--- /dev/null
+++ b/shop/api/v1/permissions.py
@@ -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",)
diff --git a/shop/migrations/0001_initial.py b/shop/migrations/0001_initial.py
index b5770ff..5a2bec8 100644
--- a/shop/migrations/0001_initial.py
+++ b/shop/migrations/0001_initial.py
@@ -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.utils.timezone
@@ -139,7 +139,12 @@ class Migration(migrations.Migration):
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",
models.DateTimeField(
@@ -346,6 +351,14 @@ class Migration(migrations.Migration):
verbose_name="Producto",
),
),
+ (
+ "tax",
+ models.ForeignKey(
+ on_delete=django.db.models.deletion.PROTECT,
+ to="shop.tax",
+ verbose_name="Impuesto aplicable",
+ ),
+ ),
],
options={
"verbose_name": "Precio de producto",
diff --git a/shop/models.py b/shop/models.py
index 7134c31..8fb05b8 100644
--- a/shop/models.py
+++ b/shop/models.py
@@ -7,11 +7,6 @@ from django.utils.text import gettext_lazy as _
class Product(models.Model):
- class UnitChoices(models.TextChoices):
- UNIT = "UNIT", _("Unidad")
- WEIGHT_KG = "KG", _("kg")
- VOLUME_LITER = "L", _("L")
-
name = models.CharField(
max_length=96,
blank=False,
@@ -30,12 +25,8 @@ class Product(models.Model):
default=Decimal("0"),
verbose_name=_("Stock"),
)
- unit = models.CharField(
- max_length=6,
- choices=UnitChoices.choices,
- default=UnitChoices.UNIT,
- verbose_name=_("Unidad de medida"),
- )
+ is_digital_asset = models.BooleanField(default=False)
+ url = models.URLField(blank=True, verbose_name=_('URL de descarga'))
def __str__(self):
return self.name
@@ -56,9 +47,19 @@ class ProductPrice(models.Model):
verbose_name=_("Producto"),
related_name="prices",
)
+ tax = models.ForeignKey(
+ "shop.Tax",
+ on_delete=models.PROTECT,
+ verbose_name=_("Impuesto aplicable"),
+ )
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:
verbose_name = _("Precio de producto")
@@ -170,7 +171,23 @@ class OrderLine(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)
+
+ status = models.CharField(
+ max_length=3, default=Statuses.STATUS_PENDING, verbose_name=_("Estado")
+ )
+
creation_date = models.DateTimeField(
auto_now_add=True, verbose_name=_("Fecha de creación")
)
diff --git a/shop/tests/test_shop_models.py b/shop/tests/test_shop_models.py
index e4fe574..3aaf5e2 100644
--- a/shop/tests/test_shop_models.py
+++ b/shop/tests/test_shop_models.py
@@ -29,9 +29,15 @@ class ShopModelsTest(TestCase):
unit=Product.UnitChoices.UNIT,
)
- ProductPrice.objects.create(product=self.potatoes, price=Decimal("0.80"))
- ProductPrice.objects.create(product=self.gasoline, price=Decimal("1.15"))
- ProductPrice.objects.create(product=self.usb_c, price=Decimal("9.95"))
+ ProductPrice.objects.create(
+ product=self.potatoes, price=Decimal("0.80"), tax=self.tax
+ )
+ 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):
self.customer = Customer.objects.create(
@@ -57,19 +63,16 @@ class ShopModelsTest(TestCase):
l1 = create_order_line_for_product(
self.potatoes,
quantity=Decimal("1.5"),
- tax=self.tax,
order=order,
)
l2 = create_order_line_for_product(
self.gasoline,
quantity=Decimal("40"),
- tax=self.tax,
order=order,
)
l3 = create_order_line_for_product(
self.usb_c,
quantity=Decimal("1.00"),
- tax=self.tax,
order=order,
)
order.calculate_total_from_lines()
diff --git a/shop/utils.py b/shop/utils.py
index 418b940..b4365c5 100644
--- a/shop/utils.py
+++ b/shop/utils.py
@@ -5,20 +5,19 @@ from shop.models import OrderLine, Tax, Product, Order, Customer
from django.db.models import QuerySet
-def create_order_line_for_product(
- product: Product, quantity: Decimal, tax: Tax, order: Order
-):
- price = product.prices.last().price
- base_total = round(price * quantity, 2)
- taxes = round(base_total * (tax.value / Decimal("100")), 2)
+def create_order_line_for_product(product: Product, quantity: Decimal, order: Order):
+ price = product.prices.last()
+ base_total = round(price.price * quantity, 2)
+ tax_value = price.tax.value / Decimal("100")
+ taxes = round(base_total * tax_value, 2)
return OrderLine.objects.create(
order=order,
product=product,
quantity=quantity,
- price=price,
+ price=price.price,
base_total=base_total,
- tax_value=tax.value,
+ tax_value=tax_value,
taxes=taxes,
total=base_total + taxes,
)
diff --git a/tpv/redsys.py b/tpv/redsys.py
index 6a1df64..556e70d 100644
--- a/tpv/redsys.py
+++ b/tpv/redsys.py
@@ -1,39 +1,36 @@
import base64
import json
+import requests
from django.conf import settings
from django.urls import reverse
+
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
-
-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
+from tpv.exceptions import RedsysPaymentException
class RedsysClient:
DEBUG_ENVIRONMENT_URL = "https://sis-t.redsys.es:25443/sis/realizarPago"
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):
if settings.DEBUG:
return self.DEBUG_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:
return settings.REDSYS_MERCHANT_CODE
@@ -66,7 +63,7 @@ class RedsysClient:
key = self.get_shared_secret()
return compute_signature(str(hash), payload, key).decode()
- def get_merchant_parameters_for_transaction(
+ def _get_merchant_parameters_for_transaction(
self,
transaction: PaymentTransaction,
transaction_type: int = TransactionTypes.AUTHORIZATION,
@@ -90,23 +87,22 @@ class RedsysClient:
"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,
transaction: PaymentTransaction,
transaction_type: int = TransactionTypes.AUTHORIZATION,
) -> str:
- body = self.get_merchant_parameters_for_transaction(
+ body = self._get_merchant_parameters_for_transaction(
transaction, transaction_type
)
- stringified_body = json.dumps(body)
- return base64.b64encode(stringified_body.encode()).decode("utf-8")
+ return self._encode_body(body)
def get_body_for_transaction(
self,
transaction: PaymentTransaction,
transaction_type: int = TransactionTypes.AUTHORIZATION,
) -> dict:
- merchant_parameters = self.get_encoded_merchant_parameters_for_transaction(
+ merchant_parameters = self._get_encoded_merchant_parameters_for_transaction(
transaction, transaction_type
)
signature = self.get_signature(transaction.hash.hex, merchant_parameters)
@@ -116,3 +112,109 @@ class RedsysClient:
"Ds_SignatureVersion": "HMAC_SHA256_V1",
"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
diff --git a/tpv/settings.py b/tpv/settings.py
index 0b8dbda..b5a2f26 100644
--- a/tpv/settings.py
+++ b/tpv/settings.py
@@ -1,6 +1,24 @@
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 = {
8: "LEK ALL",
12: "ALGERIAN DINAR DZD",
diff --git a/tpv/tests/test_redsys.py b/tpv/tests/test_redsys.py
index 405a40f..c315838 100644
--- a/tpv/tests/test_redsys.py
+++ b/tpv/tests/test_redsys.py
@@ -30,7 +30,7 @@ class TestRedsysTPV(APITestCase):
transaction.save()
client = RedsysClient()
- merchant_parameters = client.get_merchant_parameters_for_transaction(
+ merchant_parameters = client._get_merchant_parameters_for_transaction(
transaction
)
diff --git a/tpv/utils.py b/tpv/utils.py
index 0c17e1d..042cd02 100644
--- a/tpv/utils.py
+++ b/tpv/utils.py
@@ -8,10 +8,12 @@ import re
from decimal import Decimal
from django.utils.text import gettext_lazy as _
+from django.utils import timezone
from tpv.exceptions import RedsysValidationException, RedsysPaymentException
from tpv.models import PaymentTransaction
from tpv.settings import ERROR_CODES
+from tpv.signals import redsys_payment_accepted
def compute_signature(salt, payload, key):
@@ -64,10 +66,6 @@ def validate_payment_for_transaction(
'Ds_ECI': '05',
'Ds_Response_Description': 'OPERACION AUTORIZADA'
}
-
- :param request:
- :param transaction:
- :return:
"""
data = request.POST
@@ -78,7 +76,7 @@ def validate_payment_for_transaction(
_("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)
transaction_hex = result.get("Ds_Order")
@@ -96,6 +94,41 @@ def validate_payment_for_transaction(
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):
transaction.status = PaymentTransaction.StatusChoices.PAID
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))
diff --git a/tpv/views.py b/tpv/views.py
index bf7cfec..31d4aeb 100644
--- a/tpv/views.py
+++ b/tpv/views.py
@@ -41,9 +41,6 @@ def webhook(request, transaction):
try:
amount_paid = validate_payment_for_transaction(request, transaction)
pay_transaction(transaction, amount_paid)
- redsys_payment_accepted.send_robust(
- PaymentTransaction.__class__, hash=transaction.hash
- )
return HttpResponse(status=200)
except Exception as e:
diff --git a/users/__init__.py b/users/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/users/admin.py b/users/admin.py
new file mode 100644
index 0000000..8c38f3f
--- /dev/null
+++ b/users/admin.py
@@ -0,0 +1,3 @@
+from django.contrib import admin
+
+# Register your models here.
diff --git a/users/apps.py b/users/apps.py
new file mode 100644
index 0000000..88f7b17
--- /dev/null
+++ b/users/apps.py
@@ -0,0 +1,6 @@
+from django.apps import AppConfig
+
+
+class UsersConfig(AppConfig):
+ default_auto_field = "django.db.models.BigAutoField"
+ name = "users"
diff --git a/users/migrations/__init__.py b/users/migrations/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/users/models.py b/users/models.py
new file mode 100644
index 0000000..71a8362
--- /dev/null
+++ b/users/models.py
@@ -0,0 +1,3 @@
+from django.db import models
+
+# Create your models here.
diff --git a/users/templates/users/login.html b/users/templates/users/login.html
new file mode 100644
index 0000000..620b0b5
--- /dev/null
+++ b/users/templates/users/login.html
@@ -0,0 +1,45 @@
+{% extends 'base/base.html' %}
+
+{% block content %}
+
+
+{% endblock %}
diff --git a/users/templates/users/register.html b/users/templates/users/register.html
new file mode 100644
index 0000000..0f2cba5
--- /dev/null
+++ b/users/templates/users/register.html
@@ -0,0 +1,45 @@
+{% extends 'base/base.html' %}
+
+{% block content %}
+
+{% endblock %}
\ No newline at end of file
diff --git a/users/templates/users/reset_password.html b/users/templates/users/reset_password.html
new file mode 100644
index 0000000..4facf45
--- /dev/null
+++ b/users/templates/users/reset_password.html
@@ -0,0 +1,42 @@
+{% extends 'base/base.html' %}
+
+{% block content %}
+
+
+
+{% endblock %}
\ No newline at end of file
diff --git a/users/tests.py b/users/tests.py
new file mode 100644
index 0000000..7ce503c
--- /dev/null
+++ b/users/tests.py
@@ -0,0 +1,3 @@
+from django.test import TestCase
+
+# Create your tests here.
diff --git a/users/urls.py b/users/urls.py
new file mode 100644
index 0000000..e4abd46
--- /dev/null
+++ b/users/urls.py
@@ -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"),
+]
diff --git a/users/views.py b/users/views.py
new file mode 100644
index 0000000..8d0b5ab
--- /dev/null
+++ b/users/views.py
@@ -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", {})