Repository: ISPP-2425-G4/borroo
Pull request #390: Feature/backend logica negocio
Author: login: Sangros02
Labels: name: Subgrupo 2
name: Fullstack
Created: 2025-04-01T18:49:42Z | Merged: 2025-04-03T20:47:31Z

Resolves #351 

Comprobar que se puede solicitar un objeto en fechas solapadas y, después, aceptar una para ver cómo se cancela la otra

Linked issue #351: Backend logica de negocio alquileres
**Descripcion de la tarea**
Permitir crear alquileres (Rent) de un mismo objeto (Item) en la misma fecha y que al aceptar una, se anulen el resto

**Tiempo estimado**
2 dias

**Fechas de desarrollo**
Lunes 31

**Additional context**


Recuerde que cada día de trabajo debe indicar en el calendario Niko Niko su grado de satisfacción.


Diff:
diff --git a/backend/rentas/views.py b/backend/rentas/views.py
index 63577613..bb58e192 100644
--- a/backend/rentas/views.py
+++ b/backend/rentas/views.py
@@ -79,26 +79,56 @@ def first_request(self, request, *args, **kwargs):
         end_date = request.data.get('end_date')
 
         user_id = request.data.get('renter')
-        user = get_object_or_404(User, pk=user_id)
 
+        user = get_object_or_404(User, pk=user_id)
         item = get_object_or_404(Item, pk=item_id)
 
+        # Verificar campos obligatorios
+        if not all([item_id, start_date, end_date, user_id]):
+            return Response(
+                {'error': 'Faltan campos obligatorios.'},
+                status=status.HTTP_400_BAD_REQUEST
+            )
+
         not_rent_yourself = user != item.user
         is_authorized(condition=not_rent_yourself)
 
-        # Solo permitir si no hay ninguna renta activa
-        overlapping_rents = Rent.objects.filter(
+        # Bloquear si hay rentas en estado que implica que ya está reservado
+        conflicting_rents = Rent.objects.filter(
             item=item,
             start_date__lte=end_date,
-            end_date__gte=start_date
-        ).exclude(rent_status=RentStatus.CANCELLED)
-
-        if overlapping_rents.exists():
+            end_date__gte=start_date,
+            rent_status__in=[
+                RentStatus.ACCEPTED,
+                RentStatus.BOOKED,
+                RentStatus.PICKED_UP
+            ]
+        )
+        if conflicting_rents.exists():
             return Response(
                 {'error': 'El objeto no está disponible en esas fechas'},
                 status=status.HTTP_400_BAD_REQUEST
             )
 
+        # Validar que no tenga una REQUESTED propia en fechas solapadas
+        already_requested = Rent.objects.filter(
+            item=item,
+            renter=user,
+            rent_status=RentStatus.REQUESTED,
+            start_date__lte=end_date,
+            end_date__gte=start_date
+        )
+        if already_requested.exists():
+            return Response(
+                {
+                    'error': (
+                        'Ya tienes una solicitud pendiente para este objeto '
+                        'en esas fechas'
+                    )
+                },
+                status=status.HTTP_400_BAD_REQUEST
+            )
+
         serializer = RentSerializer(data=request.data)
         if serializer.is_valid():
             serializer.save(renter=user, item=item)
@@ -108,7 +138,6 @@ def first_request(self, request, *args, **kwargs):
     @action(detail=True, methods=['put'])
     def respond_request(self, request, pk=None):
         rent_id = request.data.get('rent')
-        rent = get_object_or_404(Rent, pk=rent_id)
         response = request.data.get("response")
         user_id = request.data.get('user_id')
 
@@ -120,7 +149,7 @@ def respond_request(self, request, pk=None):
                 },
                 status=status.HTTP_400_BAD_REQUEST
             )
-
+        rent = get_object_or_404(Rent, pk=rent_id)
         user = get_object_or_404(User, pk=user_id)
 
         # Validación de permisos
@@ -146,12 +175,22 @@ def respond_request(self, request, pk=None):
         if response == "accepted":
             rent.rent_status = RentStatus.ACCEPTED
             rent.save()
-            return Response(
-                {
-                    'status': 'Solicitud aceptada. '
-                              'El vendedor ha aceptado la solicitud.'
-                }
-            )
+
+            # Cancelar otras solicitudes REQUESTED solapadas
+            overlapping_requests = Rent.objects.filter(
+                item=rent.item,
+                rent_status=RentStatus.REQUESTED,
+                start_date__lt=rent.end_date,
+                end_date__gt=rent.start_date
+            ).exclude(pk=rent.pk)
+
+            overlapping_requests.update(rent_status=RentStatus.CANCELLED)
+
+            return Response({
+                'status': (
+                    'Solicitud aceptada. Solicitudes solapadas canceladas.'
+                )
+            })
 
         elif response == "rejected":
             rent.rent_status = RentStatus.CANCELLED
diff --git a/frontend/src/RentRequestBoard.jsx b/frontend/src/RentRequestBoard.jsx
index 399c8f2e..8f833d78 100644
--- a/frontend/src/RentRequestBoard.jsx
+++ b/frontend/src/RentRequestBoard.jsx
@@ -92,34 +92,40 @@ const RentRequestBoard = () => {
 
     const handleResponse = async (renta, responseType) => {
         try {
-            const user = JSON.parse(localStorage.getItem("user"));
-            if (!user || !user.id) {
-                alert("No se encontró el usuario. Asegúrate de haber iniciado sesión.");
-                return;
-            }
-            const response = await axios.put(
-                `${import.meta.env.VITE_API_BASE_URL}/rentas/full/${renta.id}/respond_request/`,
-                {
-                    response: responseType,
-                    rent: renta.id,
-                    user_id: user.id
-                }
-            );
-            console.log(response.data);
-    
-            
-            setReceivedRequests((prevRequests) =>
-                prevRequests.map((request) =>
-                    request.id === renta.id
-                        ? { ...request, rent_status: responseType, payment_status: "pending" }
-                        : request
-                )
-            );
-            setOpenModal(false);
-        } catch (error) {
-            console.error(`Error al procesar la solicitud:`, error.response?.data || error.message);
-        }
-    };
+          const user = JSON.parse(localStorage.getItem("user"));
+          if (!user || !user.id) {
+            alert("No se encontró el usuario. Asegúrate de haber iniciado sesión.");
+            return;
+          }
+      
+          const response = await axios.put(
+      `${import.meta.env.VITE_API_BASE_URL}/rentas/full/${renta.id}/respond_request/`,
+      {
+        response: responseType,
+        rent: renta.id,
+        user_id: user.id
+      }
+    );
+
+    console.log(response.data);
+    setOpenModal(false);
+
+    if (responseType === "accepted") {
+      window.location.reload(); // Solo recarga si fue aceptada
+    } else {
+      // Si fue rechazada, actualiza estado local
+      setReceivedRequests((prevRequests) =>
+        prevRequests.map((request) =>
+          request.id === renta.id
+            ? { ...request, rent_status: responseType, payment_status: "pending" }
+            : request
+        )
+      );
+    }
+  } catch (error) {
+    console.error("Error al procesar la solicitud:", error.response?.data || error.message);
+  }
+};
 
     const openConfirmModal = (renta, responseType) => {
         setSelectedRequest(renta);
diff --git a/frontend/src/ShowItem.jsx b/frontend/src/ShowItem.jsx
index 21557325..e2693745 100644
--- a/frontend/src/ShowItem.jsx
+++ b/frontend/src/ShowItem.jsx
@@ -52,7 +52,7 @@ const ShowItemScreen = () => {
   }]);
   const [showRentalModal, setShowRentalModal] = useState(false);
   const [currentImageIndex, setCurrentImageIndex] = useState(0);
-  const [requestedDates, setRequestedDates] = useState([]);
+  {/*const [requestedDates, setRequestedDates] = useState([]);*/}
   const [bookedDates, setBookedDates] = useState([]);
   const [isOwner, setIsOwner] = useState(false); // Estado para verificar si el usuario es el propietario
   const [priceCategory, setPriceCategory]= useState(null)
@@ -199,28 +199,27 @@ const ShowItemScreen = () => {
         `${import.meta.env.VITE_API_BASE_URL}/rentas/full/item/${id}/`
       );
       const rents = rentResponse.data;
-
-      const requested = [];
+  
       const booked = [];
-      
+  
       rents.forEach((rent) => {
         const start = new Date(rent.start_date);
         const end = new Date(rent.end_date);
         const days = getDatesInRange(start, end);
-        
-        if (rent.rent_status === "requested") {
-          requested.push(...days);
-        } else if (rent.rent_status === "BOOKED") {
+  
+        const status = rent.rent_status.toLowerCase();
+  
+        if (["accepted", "booked", "picked_up"].includes(status)) {
           booked.push(...days);
         }
       });
-
-      setRequestedDates(requested);
+  
       setBookedDates(booked);
     } catch (error) {
       console.error("Error fetching availability:", error);
     }
   };
+  
   const getDatesInRange = (startDate, endDate) => {
     const dates = [];
     for (let d = new Date(startDate); d <= endDate; d.setDate(d.getDate() + 1)) {
@@ -765,7 +764,7 @@ const ShowItemScreen = () => {
               }
             }}
             minDate={new Date()}
-            disabledDates={[...requestedDates, ...bookedDates, ...unavailabilityPeriods.flatMap(period => {
+            disabledDates={[ ...bookedDates, ...unavailabilityPeriods.flatMap(period => {
               const start = new Date(period.start_date);
               const end = new Date(period.end_date);
               const range = getDatesInRange(start, end);


Reviews:
user: login: copilot-pull-request-reviewer[bot]
body: ## Pull Request Overview

This PR implements backend business logic to handle overlapping rental requests by allowing a request during conflicting dates and canceling overlapping ones when a request is accepted. Key changes include:
- Frontend: The "requestedDates" state is commented out and the disabled dates in the date picker now only include booked dates.
- Frontend (RentRequestBoard): Updates in modal handling and state management to trigger a reload only when a request is accepted.
- Backend: Enhancements in the rental request validation and response endpoints, including a refined conflict check and cancellation of overlapping pending requests upon acceptance.

### Reviewed Changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 1 comment.

| File                        | Description                                                                                       |
| --------------------------- | ------------------------------------------------------------------------------------------------- |
| frontend/src/ShowItem.jsx   | Removed usage of the requestedDates state and updated disabled dates based on the updated logic.  |
| frontend/src/RentRequestBoard.jsx | Refactored modal closure and UI update handling after rental response.                      |
| backend/rentas/views.py     | Reordered mandatory field validation and introduced cancellation of overlapping rental requests.  |




state: COMMENTED
author_association: NONE
submitted_at: 2025-04-01T18:50:36Z
user: login: frafermot
body: Hay conflictos, mergea develop en tu rama para resolverlos
state: CHANGES_REQUESTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-01T21:50:13Z
user: login: frafermot
body: Todo correcto
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-04-03T20:42:15Z

Review comments:
diff_hunk: @@ -79,26 +79,56 @@ def first_request(self, request, *args, **kwargs):
         end_date = request.data.get('end_date')
 
         user_id = request.data.get('renter')
-        user = get_object_or_404(User, pk=user_id)
 
+        user = get_object_or_404(User, pk=user_id)
         item = get_object_or_404(Item, pk=item_id)
 
path: backend/rentas/views.py
user: login: Copilot
body: Consider moving the mandatory fields check (item_id, start_date, end_date, user_id) before fetching the user to avoid triggering a 404 error when user_id is missing.
created_at: 2025-04-01T18:50:35Z
author_association: NONE

PR comments:
user: login: Sangros02
created_at: 2025-04-01T18:50:31Z
body: Esperar a que se haga un merge de develop
author_association: CONTRIBUTOR
user: login: sonarqubecloud[bot]
created_at: 2025-04-02T12:21:59Z
body: ## [![Quality Gate Passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/qg-passed-20px.png 'Quality Gate Passed')](https://sonarcloud.io/dashboard?id=ISPP-2425-G4_borroo&pullRequest=390) **Quality Gate passed**  
Issues  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [2 New issues](https://sonarcloud.io/project/issues?id=ISPP-2425-G4_borroo&pullRequest=390&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/accepted-16px.png '') [0 Accepted issues](https://sonarcloud.io/project/issues?id=ISPP-2425-G4_borroo&pullRequest=390&issueStatuses=ACCEPTED)

Measures  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=ISPP-2425-G4_borroo&pullRequest=390&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0.0% Coverage on New Code](https://sonarcloud.io/component_measures?id=ISPP-2425-G4_borroo&pullRequest=390&metric=new_coverage&view=list)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0.0% Duplication on New Code](https://sonarcloud.io/component_measures?id=ISPP-2425-G4_borroo&pullRequest=390&metric=new_duplicated_lines_density&view=list)  
  
[See analysis details on SonarQube Cloud](https://sonarcloud.io/dashboard?id=ISPP-2425-G4_borroo&pullRequest=390)


author_association: NONE