diff --git a/tienda_ilusion/don_confiao/admin.py b/tienda_ilusion/don_confiao/admin.py index bb3ceed..0996bb0 100644 --- a/tienda_ilusion/don_confiao/admin.py +++ b/tienda_ilusion/don_confiao/admin.py @@ -10,6 +10,12 @@ from .models.sales import ( ) from .models.products import Product, ProductCategory from .models.payments import ReconciliationJar +from .models.store_settings import StoreSettings + + +@admin.register(StoreSettings) +class StoreSettingsAdmin(admin.ModelAdmin): + list_display = ("id", "address", "latitude", "longitude", "updated_at") @admin.register(Customer) diff --git a/tienda_ilusion/don_confiao/api/__init__.py b/tienda_ilusion/don_confiao/api/__init__.py index 275997b..faefa82 100644 --- a/tienda_ilusion/don_confiao/api/__init__.py +++ b/tienda_ilusion/don_confiao/api/__init__.py @@ -18,6 +18,7 @@ from .payments import ( Pagination, ) from .admin import AdminCodeValidateView +from .store_settings import StoreSettingsView __all__ = [ # Catalogue Images @@ -44,4 +45,6 @@ __all__ = [ "Pagination", # Admin "AdminCodeValidateView", + # Store Settings + "StoreSettingsView", ] diff --git a/tienda_ilusion/don_confiao/api/store_settings.py b/tienda_ilusion/don_confiao/api/store_settings.py new file mode 100644 index 0000000..dc1c585 --- /dev/null +++ b/tienda_ilusion/don_confiao/api/store_settings.py @@ -0,0 +1,28 @@ +from rest_framework import status +from rest_framework.permissions import AllowAny, IsAuthenticated +from rest_framework.response import Response +from rest_framework.views import APIView + +from ..models.store_settings import StoreSettings +from ..permissions import IsAdministrator +from ..serializers.store_settings import StoreSettingsSerializer + + +class StoreSettingsView(APIView): + def get_permissions(self): + if self.request.method == "GET": + return [AllowAny()] + return [IsAuthenticated(), IsAdministrator()] + + def get(self, request): + settings = StoreSettings.get_singleton() + return Response(StoreSettingsSerializer(settings).data) + + def patch(self, request): + settings = StoreSettings.get_singleton() + serializer = StoreSettingsSerializer( + settings, data=request.data, partial=True + ) + serializer.is_valid(raise_exception=True) + serializer.save() + return Response(serializer.data, status=status.HTTP_200_OK) diff --git a/tienda_ilusion/don_confiao/migrations/0050_storesettings.py b/tienda_ilusion/don_confiao/migrations/0050_storesettings.py new file mode 100644 index 0000000..0f0de17 --- /dev/null +++ b/tienda_ilusion/don_confiao/migrations/0050_storesettings.py @@ -0,0 +1,27 @@ +# Generated by Django 5.0.6 on 2026-08-08 17:33 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('don_confiao', '0049_catalogueimage'), + ] + + operations = [ + migrations.CreateModel( + name='StoreSettings', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('address', models.TextField()), + ('latitude', models.FloatField(blank=True, null=True)), + ('longitude', models.FloatField(blank=True, null=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ], + options={ + 'verbose_name': 'Configuración de la tienda', + 'verbose_name_plural': 'Configuración de la tienda', + }, + ), + ] diff --git a/tienda_ilusion/don_confiao/models/__init__.py b/tienda_ilusion/don_confiao/models/__init__.py index e69de29..147edfd 100644 --- a/tienda_ilusion/don_confiao/models/__init__.py +++ b/tienda_ilusion/don_confiao/models/__init__.py @@ -0,0 +1,3 @@ +from .store_settings import StoreSettings + +__all__ = ["StoreSettings"] diff --git a/tienda_ilusion/don_confiao/models/store_settings.py b/tienda_ilusion/don_confiao/models/store_settings.py new file mode 100644 index 0000000..d7fe4dd --- /dev/null +++ b/tienda_ilusion/don_confiao/models/store_settings.py @@ -0,0 +1,20 @@ +from django.db import models + + +class StoreSettings(models.Model): + address = models.TextField() + latitude = models.FloatField(null=True, blank=True) + longitude = models.FloatField(null=True, blank=True) + updated_at = models.DateTimeField(auto_now=True) + + class Meta: + verbose_name = "Configuración de la tienda" + verbose_name_plural = "Configuración de la tienda" + + def __str__(self): + return f"StoreSettings (id={self.id})" + + @classmethod + def get_singleton(cls): + instance, _ = cls.objects.get_or_create(pk=1) + return instance diff --git a/tienda_ilusion/don_confiao/serializers/__init__.py b/tienda_ilusion/don_confiao/serializers/__init__.py index ff3b7b2..83f093b 100644 --- a/tienda_ilusion/don_confiao/serializers/__init__.py +++ b/tienda_ilusion/don_confiao/serializers/__init__.py @@ -16,6 +16,7 @@ from .payments import ( ReconciliationJarSerializer, PaymentMethodSerializer, ) +from .store_settings import StoreSettingsSerializer __all__ = [ # Catalogue Images @@ -39,4 +40,6 @@ __all__ = [ # Payments "ReconciliationJarSerializer", "PaymentMethodSerializer", + # Store Settings + "StoreSettingsSerializer", ] diff --git a/tienda_ilusion/don_confiao/serializers/store_settings.py b/tienda_ilusion/don_confiao/serializers/store_settings.py new file mode 100644 index 0000000..20ac4b8 --- /dev/null +++ b/tienda_ilusion/don_confiao/serializers/store_settings.py @@ -0,0 +1,10 @@ +from rest_framework import serializers + +from ..models.store_settings import StoreSettings + + +class StoreSettingsSerializer(serializers.ModelSerializer): + class Meta: + model = StoreSettings + fields = ["id", "address", "latitude", "longitude", "updated_at"] + read_only_fields = ["id", "updated_at"] diff --git a/tienda_ilusion/don_confiao/tests/test_store_settings.py b/tienda_ilusion/don_confiao/tests/test_store_settings.py new file mode 100644 index 0000000..1795987 --- /dev/null +++ b/tienda_ilusion/don_confiao/tests/test_store_settings.py @@ -0,0 +1,157 @@ +from django.contrib.auth.models import User +from rest_framework import status +from rest_framework.test import APIClient, APITestCase +from rest_framework_simplejwt.tokens import RefreshToken + +from ..models.store_settings import StoreSettings +from .Mixins import LoginMixin + +URL = "/don_confiao/api/store_settings" + + +class TestStoreSettingsModel(APITestCase, LoginMixin): + def test_singleton_returns_single_instance(self): + first = StoreSettings.get_singleton() + second = StoreSettings.get_singleton() + self.assertEqual(first.pk, second.pk) + self.assertEqual(StoreSettings.objects.count(), 1) + + def test_singleton_created_without_address(self): + instance = StoreSettings.get_singleton() + self.assertEqual(instance.address, "") + + def test_str(self): + instance = StoreSettings.get_singleton() + self.assertEqual(str(instance), f"StoreSettings (id={instance.id})") + + +class TestStoreSettingsAPIPermissions(APITestCase, LoginMixin): + def setUp(self): + self.url = URL + + def test_get_unauthenticated(self): + client = APIClient() + response = client.get(self.url) + self.assertEqual(response.status_code, status.HTTP_200_OK) + + def test_patch_unauthenticated(self): + client = APIClient() + response = client.patch( + self.url, + {"address": "Calle falsa 123"}, + format="json", + ) + self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED) + + def test_patch_non_admin(self): + user = User.objects.create_user( + username="regularuser", + email="regular@example.com", + password="regularpass", + ) + refresh = RefreshToken.for_user(user) + client = APIClient() + client.credentials( + HTTP_AUTHORIZATION=f"Bearer {str(refresh.access_token)}" + ) + response = client.patch( + self.url, + {"address": "Calle falsa 123"}, + format="json", + ) + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + + def test_patch_publico(self): + user = User.objects.create_user( + username="publico", + email="publico@example.com", + password="publicopass", + ) + user.profile.user_type = "publico" + user.profile.save() + refresh = RefreshToken.for_user(user) + client = APIClient() + client.credentials( + HTTP_AUTHORIZATION=f"Bearer {str(refresh.access_token)}" + ) + response = client.patch( + self.url, + {"address": "Calle falsa 123"}, + format="json", + ) + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + + +class TestStoreSettingsAPI(APITestCase, LoginMixin): + def setUp(self): + self.login() + self.url = URL + + def test_get_creates_singleton(self): + self.assertEqual(StoreSettings.objects.count(), 0) + response = self.client.get(self.url) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(StoreSettings.objects.count(), 1) + data = response.json() + self.assertIn("id", data) + self.assertIn("address", data) + self.assertIn("latitude", data) + self.assertIn("longitude", data) + self.assertIn("updated_at", data) + + def test_patch_updates_settings(self): + self.client.get(self.url) + response = self.client.patch( + self.url, + { + "address": "Carrera 5 # 10-20, Bogotá", + "latitude": 4.6097, + "longitude": -74.0817, + }, + format="json", + ) + self.assertEqual(response.status_code, status.HTTP_200_OK) + data = response.json() + self.assertEqual(data["address"], "Carrera 5 # 10-20, Bogotá") + self.assertEqual(data["latitude"], 4.6097) + self.assertEqual(data["longitude"], -74.0817) + + settings = StoreSettings.objects.get(pk=1) + self.assertEqual(settings.address, "Carrera 5 # 10-20, Bogotá") + self.assertEqual(settings.latitude, 4.6097) + self.assertEqual(settings.longitude, -74.0817) + + def test_patch_partial(self): + self.client.get(self.url) + response = self.client.patch( + self.url, + {"address": "Solo dirección"}, + format="json", + ) + self.assertEqual(response.status_code, status.HTTP_200_OK) + settings = StoreSettings.objects.get(pk=1) + self.assertEqual(settings.address, "Solo dirección") + self.assertIsNone(settings.latitude) + + def test_patch_reuses_singleton(self): + self.client.get(self.url) + self.client.patch( + self.url, + {"address": "Primera"}, + format="json", + ) + self.client.patch( + self.url, + {"address": "Segunda"}, + format="json", + ) + self.assertEqual(StoreSettings.objects.count(), 1) + + def test_patch_invalid_coordinates(self): + self.client.get(self.url) + response = self.client.patch( + self.url, + {"latitude": "not-a-number"}, + format="json", + ) + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) diff --git a/tienda_ilusion/don_confiao/urls.py b/tienda_ilusion/don_confiao/urls.py index e7e5592..929207c 100644 --- a/tienda_ilusion/don_confiao/urls.py +++ b/tienda_ilusion/don_confiao/urls.py @@ -26,6 +26,8 @@ from .api import ( SalesForReconciliationView, # Admin AdminCodeValidateView, + # Store Settings + StoreSettingsView, ) app_name = "don_confiao" @@ -95,4 +97,5 @@ urlpatterns = [ AdminCodeValidateView.as_view(), ), path("api/sales/for_tryton", SalesForTrytonView.as_view()), + path("api/store_settings", StoreSettingsView.as_view()), ]