# TOTES-SAC — Correcciones Requeridas antes de QA
**Versión:** 1.0 | **Fecha:** Mayo 2025  
**Sistema:** Sistema de Atención al Cliente (Next.js + Prisma + SQLite)  
**Stack aceptado:** Next.js 16, Prisma, SQLite, almacenamiento local — sin cambios de stack requeridos.  
**Nota sobre app móvil:** La app React Native + Expo (iOS y Android) queda pendiente para la segunda entrega. Este documento cubre únicamente los 9 bugs del sistema web actual.

---

## Índice

| # | Bug | Severidad | Archivo(s) afectado(s) |
|---|-----|-----------|------------------------|
| 1 | `internalNote` expuesta al cliente vía API | 🔴 Crítico | `src/app/api/reviews/[id]/route.ts` |
| 2 | `respond` no marca la reseña como `RESOLVED` | 🔴 Crítico | `src/app/api/reviews/[id]/respond/route.ts` |
| 3 | `SUPERVISOR` puede cambiar estado manualmente | 🔴 Crítico | `src/app/api/reviews/[id]/status/route.ts` |
| 4 | Fotos accesibles entre clientes distintos | 🔴 Crítico | `src/app/api/reviews/[id]/photos/route.ts` |
| 5 | No existe endpoint `register-push-token` | 🟡 Gap funcional | `src/app/api/auth/register-push-token/route.ts` (crear) |
| 6 | Sin escalamiento automático a las 24h | 🟡 Gap funcional | `src/app/api/cron/escalate/route.ts` (crear) |
| 7 | Sin validación de reseña única por servicio | 🟡 Gap funcional | `src/app/api/reviews/route.ts` |
| 8 | `DELETE` es hard delete, no soft delete | 🟡 Riesgo operativo | `prisma/schema.prisma` + `src/app/api/reviews/[id]/route.ts` |
| 9 | Registro público de clientes sin autenticación | 🟡 Gap funcional | `src/app/api/auth/register/route.ts` |

---

## Bug 1 — `internalNote` expuesta al cliente vía API 🔴

### Problema
`GET /api/reviews/[id]` retorna el objeto completo de `responses[]` incluyendo `internalNote` y `actionTaken` sin filtrar por rol. Un cliente autenticado puede leer las notas internas del supervisor haciendo un fetch directo al endpoint.

La especificación es explícita:
> "El cliente puede ver la respuesta del supervisor pero **no las notas internas**."

### Archivo a modificar
`src/app/api/reviews/[id]/route.ts`

### Cambio requerido
En el `GET`, después de obtener la reseña de la base de datos, filtrar los campos sensibles cuando el caller es `CUSTOMER`.

```ts
// src/app/api/reviews/[id]/route.ts

export async function GET(
  request: NextRequest,
  { params }: { params: Promise<{ id: string }> }
) {
  try {
    const authResult = await requireAuth()(request)
    if ('error' in authResult) return authResult.error

    const { user } = authResult
    const { id } = await params

    const review = await db.review.findUnique({
      where: { id },
      include: {
        customer: { select: { id: true, name: true, email: true, phone: true } },
        assignedTo: { select: { id: true, name: true, email: true } },
        photos: { orderBy: { uploadedAt: 'asc' } },
        responses: {
          include: {
            supervisor: { select: { id: true, name: true, email: true } },
          },
          orderBy: { createdAt: 'desc' },
        },
      },
    })

    if (!review) {
      return NextResponse.json({ error: 'Reseña no encontrada' }, { status: 404 })
    }

    // CUSTOMER solo puede ver sus propias reseñas
    if (user.role === 'CUSTOMER' && review.customerId !== user.id) {
      return NextResponse.json({ error: 'Acceso denegado' }, { status: 403 })
    }

    // ✅ NUEVO: Filtrar campos internos cuando el caller es CUSTOMER
    if (user.role === 'CUSTOMER') {
      const filteredReview = {
        ...review,
        responses: review.responses.map(r => ({
          id: r.id,
          reviewId: r.reviewId,
          supervisorId: r.supervisorId,
          supervisor: r.supervisor,
          responseToClient: r.responseToClient,   // ✅ Visible para el cliente
          createdAt: r.createdAt,
          // internalNote y actionTaken: NO se incluyen
        })),
      }
      return NextResponse.json({ review: filteredReview })
    }

    return NextResponse.json({ review })
  } catch (error) {
    console.error('Error al obtener reseña:', error)
    return NextResponse.json({ error: 'Error interno del servidor' }, { status: 500 })
  }
}
```

### Verificación
- Loguear como CUSTOMER → GET `/api/reviews/{id}` → el JSON **no debe** contener `internalNote` ni `actionTaken`.
- Loguear como SUPERVISOR o MANAGER → el JSON **sí debe** contener ambos campos.

---

## Bug 2 — `respond` no marca la reseña como `RESOLVED` 🔴

### Problema
`PUT /api/reviews/[id]/respond` solo lleva la reseña de `PENDING` a `ACKNOWLEDGED`, pero nunca a `RESOLVED`. Según el flujo definido:

> "Supervisor registra acciones tomadas y respuesta al cliente → **Status = Resolved**"

El supervisor actualmente tendría que ir manualmente a cambiar el estado a `RESOLVED` por separado, lo cual rompe el ciclo de vida automatizado.

### Archivo a modificar
`src/app/api/reviews/[id]/respond/route.ts`

### Cambio requerido
Reemplazar la lógica de actualización de estado para que siempre lleve la reseña a `RESOLVED` al registrar la respuesta.

```ts
// src/app/api/reviews/[id]/respond/route.ts
// Reemplazar el bloque de actualización de estado (actualmente solo pasa a ACKNOWLEDGED)

    // ✅ ANTES (código actual — INCORRECTO):
    // if (review.status === 'PENDING') {
    //   await db.review.update({
    //     where: { id },
    //     data: { status: 'ACKNOWLEDGED', notifiedAt: review.notifiedAt || new Date() },
    //   })
    // }

    // ✅ DESPUÉS (código correcto):
    // Al registrar la respuesta, la reseña siempre pasa a RESOLVED
    await db.review.update({
      where: { id },
      data: {
        status: 'RESOLVED',
        resolvedAt: new Date(),
        notifiedAt: review.notifiedAt || new Date(),
      },
    })
```

El bloque completo del `PUT` después del cambio:

```ts
export async function PUT(
  request: NextRequest,
  { params }: { params: Promise<{ id: string }> }
) {
  try {
    const authResult = await requireAuth(['SUPERVISOR'])(request)
    if ('error' in authResult) return authResult.error

    const { user } = authResult
    const { id } = await params
    const body = await request.json()
    const { actionTaken, internalNote, responseToClient } = body

    if (!actionTaken || !responseToClient) {
      return NextResponse.json(
        { error: 'Acciones tomadas y respuesta al cliente son requeridas' },
        { status: 400 }
      )
    }

    const review = await db.review.findUnique({
      where: { id },
      include: { customer: { select: { id: true, name: true } } },
    })

    if (!review) {
      return NextResponse.json({ error: 'Reseña no encontrada' }, { status: 404 })
    }

    const response = await db.supervisorResponse.create({
      data: {
        reviewId: id,
        supervisorId: user.id,
        actionTaken: actionTaken.trim(),
        internalNote: internalNote?.trim() || '',
        responseToClient: responseToClient.trim(),
      },
    })

    // ✅ Marcar la reseña como RESOLVED al registrar la respuesta
    await db.review.update({
      where: { id },
      data: {
        status: 'RESOLVED',
        resolvedAt: new Date(),
        notifiedAt: review.notifiedAt || new Date(),
      },
    })

    // Notificar al cliente
    await notifyUsers({
      userIds: [review.customerId],
      title: 'Respuesta a su reseña',
      message: `Su reseña ${review.reviewCode} ha recibido una respuesta. ${responseToClient}`,
      type: 'RESPONSE_TO_CLIENT',
      reviewId: review.id,
    })

    return NextResponse.json({ response })
  } catch (error) {
    console.error('Error al responder reseña:', error)
    return NextResponse.json({ error: 'Error interno del servidor' }, { status: 500 })
  }
}
```

### Verificación
- Crear reseña negativa → status debe ser `PENDING`.
- Supervisor responde → status debe cambiar a `RESOLVED` y `resolvedAt` debe tener fecha.
- En el dashboard del supervisor, la reseña debe desaparecer de "Pendientes" y aparecer en "Resueltas".

---

## Bug 3 — `SUPERVISOR` puede cambiar estado manualmente 🔴

### Problema
`PUT /api/reviews/[id]/status` permite tanto `SUPERVISOR` como `MANAGER`. La especificación indica:

> "Cambiar estado manualmente → **Gerencia** (MANAGER únicamente)"

Esto le da al supervisor poder de reabrir o forzar estados arbitrariamente, incluyendo reabrir reseñas ya resueltas, lo cual el spec prohíbe explícitamente para roles que no sean gerencia.

### Archivo a modificar
`src/app/api/reviews/[id]/status/route.ts`

### Cambio requerido
Una sola línea — cambiar el array de roles permitidos:

```ts
// ✅ ANTES (incorrecto):
const authResult = await requireAuth(['SUPERVISOR', 'MANAGER'])(request)

// ✅ DESPUÉS (correcto):
const authResult = await requireAuth(['MANAGER'])(request)
```

### Verificación
- Loguear como SUPERVISOR → `PUT /api/reviews/{id}/status` → debe retornar `403 Forbidden`.
- Loguear como MANAGER → debe funcionar normalmente.

---

## Bug 4 — Fotos accesibles entre clientes distintos 🔴

### Problema
`GET /api/reviews/[id]/photos` verifica que la reseña exista, pero no verifica que si el caller es `CUSTOMER`, la reseña le pertenezca. Un cliente puede consultar las fotos de la reseña de otro cliente si conoce o adivina el `id` de la reseña.

### Archivo a modificar
`src/app/api/reviews/[id]/photos/route.ts`

### Cambio requerido
Agregar validación de propiedad en el `GET`, igual a como se hace en `GET /api/reviews/[id]`:

```ts
export async function GET(
  request: NextRequest,
  { params }: { params: Promise<{ id: string }> }
) {
  try {
    const authResult = await requireAuth()(request)
    if ('error' in authResult) return authResult.error

    const { user } = authResult  // ✅ Necesitamos el user para validar
    const { id: reviewId } = await params

    const review = await db.review.findUnique({ where: { id: reviewId } })
    if (!review) {
      return NextResponse.json({ error: 'Reseña no encontrada' }, { status: 404 })
    }

    // ✅ NUEVO: CUSTOMER solo puede ver fotos de sus propias reseñas
    if (user.role === 'CUSTOMER' && review.customerId !== user.id) {
      return NextResponse.json({ error: 'Acceso denegado' }, { status: 403 })
    }

    const photos = await db.reviewPhoto.findMany({
      where: { reviewId },
      orderBy: { uploadedAt: 'asc' },
    })

    return NextResponse.json({ photos })
  } catch (error) {
    console.error('Error al obtener fotos:', error)
    return NextResponse.json({ error: 'Error interno del servidor' }, { status: 500 })
  }
}
```

### Verificación
- Cliente A intenta `GET /api/reviews/{id_reseña_cliente_B}/photos` → debe retornar `403`.
- Cliente A consulta fotos de su propia reseña → debe retornar `200` con las fotos.
- Supervisor o Manager consultan fotos de cualquier reseña → debe funcionar normalmente.

---

## Bug 5 — No existe endpoint `register-push-token` 🟡

### Problema
El modelo `User` tiene el campo `expoPushToken` en la base de datos, pero no existe el endpoint `POST /api/auth/register-push-token` que permite a los dispositivos registrar su token. Sin este endpoint, las notificaciones push (cuando se implemente la app móvil en la segunda entrega) no podrán funcionar.

### Archivo a crear
`src/app/api/auth/register-push-token/route.ts`

```ts
// src/app/api/auth/register-push-token/route.ts
import { NextRequest, NextResponse } from 'next/server'
import { db } from '@/lib/db'
import { requireAuth } from '@/lib/api-auth'

export async function POST(request: NextRequest) {
  try {
    const authResult = await requireAuth()(request)
    if ('error' in authResult) return authResult.error

    const { user } = authResult
    const body = await request.json()
    const { token } = body

    if (!token || typeof token !== 'string') {
      return NextResponse.json(
        { error: 'Token de push requerido' },
        { status: 400 }
      )
    }

    await db.user.update({
      where: { id: user.id },
      data: { expoPushToken: token.trim() },
    })

    return NextResponse.json({ message: 'Token registrado correctamente' })
  } catch (error) {
    console.error('Error al registrar push token:', error)
    return NextResponse.json({ error: 'Error interno del servidor' }, { status: 500 })
  }
}
```

### Verificación
- `POST /api/auth/register-push-token` con `{ "token": "ExponentPushToken[xxx]" }` → `200`.
- Verificar en la BD que el campo `expoPushToken` del usuario se actualizó.
- Sin token en el body → `400 Bad Request`.

---

## Bug 6 — Sin escalamiento automático a las 24 horas 🟡

### Problema
El spec define:
> "El supervisor tiene máximo 24 horas hábiles para responder una reseña negativa antes de que se escale automáticamente a gerencia."

Actualmente el dashboard de gerencia solo *muestra* el conteo de reseñas sin atender que superaron 24h, pero no hay ningún proceso automático que envíe la notificación de escalamiento.

### Solución
Crear un endpoint de cron protegido por una clave secreta, que sea invocado cada hora por un proceso externo (cron del servidor, GitHub Actions Scheduled, etc.).

### Paso 1 — Agregar variable de entorno
En el archivo `.env`:
```
CRON_SECRET=una_clave_secreta_larga_y_aleatoria_aqui
```

### Paso 2 — Crear el endpoint de cron
`src/app/api/cron/escalate/route.ts`

```ts
// src/app/api/cron/escalate/route.ts
import { NextRequest, NextResponse } from 'next/server'
import { db } from '@/lib/db'
import { notifyUsers } from '@/lib/notifications'

export async function GET(request: NextRequest) {
  // Validar que la llamada viene del proceso autorizado
  const authHeader = request.headers.get('authorization')
  if (authHeader !== `Bearer ${process.env.CRON_SECRET}`) {
    return NextResponse.json({ error: 'No autorizado' }, { status: 401 })
  }

  try {
    const twentyFourHoursAgo = new Date(Date.now() - 24 * 60 * 60 * 1000)

    // Buscar reseñas negativas PENDING sin notificación de escalamiento
    // (solo escalar las que aún no fueron escaladas: notifiedAt < 24h ago o notifiedAt es null)
    const overdueReviews = await db.review.findMany({
      where: {
        status: 'PENDING',
        sentiment: 'NEGATIVE',
        createdAt: { lte: twentyFourHoursAgo },
      },
      include: {
        customer: { select: { id: true, name: true } },
        assignedTo: { select: { id: true, name: true } },
      },
    })

    if (overdueReviews.length === 0) {
      return NextResponse.json({ escalated: 0 })
    }

    // Obtener todos los managers activos
    const managers = await db.user.findMany({
      where: { role: 'MANAGER', isActive: true },
      select: { id: true },
    })

    let escalatedCount = 0

    for (const review of overdueReviews) {
      // Notificar a todos los managers
      if (managers.length > 0) {
        await notifyUsers({
          userIds: managers.map(m => m.id),
          title: '🔴 Reseña sin atender — Escalamiento',
          message: `La reseña ${review.reviewCode} del cliente ${review.customer.name} lleva más de 24 horas sin respuesta del supervisor${review.assignedTo ? ` (asignado: ${review.assignedTo.name})` : ''}.`,
          type: 'ESCALATION',
          reviewId: review.id,
        })
      }

      escalatedCount++
    }

    return NextResponse.json({
      escalated: escalatedCount,
      reviewCodes: overdueReviews.map(r => r.reviewCode),
    })
  } catch (error) {
    console.error('Error en cron de escalamiento:', error)
    return NextResponse.json({ error: 'Error interno del servidor' }, { status: 500 })
  }
}
```

### Paso 3 — Configurar el cron en el servidor
En el servidor Ubuntu, agregar al crontab (`crontab -e`):

```bash
# Ejecutar cada hora para detectar reseñas sin atender en 24h
0 * * * * curl -s -H "Authorization: Bearer una_clave_secreta_larga_y_aleatoria_aqui" http://localhost:3000/api/cron/escalate >> /var/log/totes-escalate.log 2>&1
```

### Verificación
- Crear una reseña negativa y modificar manualmente `createdAt` en la BD para que sea hace más de 24h.
- Invocar `GET /api/cron/escalate` con el header `Authorization: Bearer {CRON_SECRET}`.
- Verificar que los managers recibieron una notificación de tipo `ESCALATION`.
- Invocar sin el header o con clave incorrecta → `401`.

---

## Bug 7 — Sin validación de reseña única por servicio 🟡

### Problema
El spec define:
> "El cliente solo puede enviar una reseña por servicio recibido — se valida por `ServiceDate` y `CustomerId`."

Actualmente un cliente puede crear múltiples reseñas para el mismo tipo de servicio en la misma fecha.

### Archivo a modificar
`src/app/api/reviews/route.ts`

### Cambio requerido
Agregar validación de duplicado antes de crear la reseña en el `POST`:

```ts
// Agregar DESPUÉS de la validación de campos requeridos y ANTES de calcular el sentiment
// En el bloque POST de src/app/api/reviews/route.ts

    // ✅ NUEVO: Validar que el cliente no tenga ya una reseña para este servicio en esta fecha
    const serviceDateParsed = new Date(serviceDate)
    // Normalizar a solo la fecha (sin hora) para comparar por día
    const dayStart = new Date(serviceDateParsed)
    dayStart.setHours(0, 0, 0, 0)
    const dayEnd = new Date(serviceDateParsed)
    dayEnd.setHours(23, 59, 59, 999)

    const existingReview = await db.review.findFirst({
      where: {
        customerId: user.id,
        serviceType,
        serviceDate: {
          gte: dayStart,
          lte: dayEnd,
        },
      },
    })

    if (existingReview) {
      return NextResponse.json(
        {
          error: 'Ya existe una reseña para este tipo de servicio en esa fecha.',
          existingReviewCode: existingReview.reviewCode,
        },
        { status: 409 }
      )
    }
```

### Verificación
- Crear reseña para "Limpieza" el 2025-05-01 → `201 Created`.
- Intentar crear otra reseña para "Limpieza" el 2025-05-01 → `409 Conflict` con el código de la reseña existente.
- Crear reseña para "Limpieza" el 2025-05-02 (fecha distinta) → `201 Created` (permitido).
- Crear reseña para "Mantenimiento" el 2025-05-01 (tipo distinto) → `201 Created` (permitido).

---

## Bug 8 — `DELETE` es hard delete, no soft delete 🟡

### Problema
`DELETE /api/reviews/[id]` ejecuta `db.review.delete()` eliminando el registro permanentemente. El spec indica soft delete. Borrar reseñas de forma irreversible es un riesgo de auditoría: si gerencia elimina una reseña por error, se pierde toda la trazabilidad.

### Paso 1 — Agregar campo `deletedAt` al schema de Prisma
`prisma/schema.prisma` — agregar el campo en el modelo `Review`:

```prisma
model Review {
  // ... campos existentes ...
  resolvedAt      DateTime?
  createdAt       DateTime           @default(now())
  updatedAt       DateTime           @updatedAt
  deletedAt       DateTime?          // ✅ NUEVO: null = activa, fecha = eliminada

  photos          ReviewPhoto[]
  responses       SupervisorResponse[]

  @@map("reviews")
}
```

Después de modificar el schema, ejecutar la migración:
```bash
npx prisma migrate dev --name add_soft_delete_to_reviews
```

### Paso 2 — Modificar el endpoint DELETE
`src/app/api/reviews/[id]/route.ts`:

```ts
export async function DELETE(
  request: NextRequest,
  { params }: { params: Promise<{ id: string }> }
) {
  try {
    const authResult = await requireAuth(['MANAGER'])(request)
    if ('error' in authResult) return authResult.error

    const { id } = await params

    const review = await db.review.findUnique({
      where: { id, deletedAt: null },  // ✅ Solo buscar reseñas activas
    })

    if (!review) {
      return NextResponse.json({ error: 'Reseña no encontrada' }, { status: 404 })
    }

    // ✅ Soft delete: marcar como eliminada sin borrar el registro
    await db.review.update({
      where: { id },
      data: { deletedAt: new Date() },
    })

    return NextResponse.json({ message: 'Reseña eliminada exitosamente' })
  } catch (error) {
    console.error('Error al eliminar reseña:', error)
    return NextResponse.json({ error: 'Error interno del servidor' }, { status: 500 })
  }
}
```

### Paso 3 — Filtrar reseñas eliminadas en los listados
Agregar `deletedAt: null` en todos los `findMany` y `findUnique` relevantes:

**`src/app/api/reviews/route.ts`** — en el `where` del `GET`:
```ts
const where: Prisma.ReviewWhereInput = {
  deletedAt: null,  // ✅ NUEVO
}
```

**`src/app/api/reviews/my/route.ts`** — en el `where` del `GET`:
```ts
const where: Prisma.ReviewWhereInput = {
  customerId: user.id,
  deletedAt: null,  // ✅ NUEVO
}
```

**`src/app/api/dashboard/manager/route.ts`** y **`supervisor/route.ts`** — agregar `deletedAt: null` en todos los `db.review.count()` y `db.review.findMany()`.

### Verificación
- `DELETE /api/reviews/{id}` → responde `200`, pero el registro sigue en la BD con `deletedAt` poblado.
- `GET /api/reviews` → la reseña eliminada ya no aparece en los listados.
- `GET /api/reviews/{id}` de una reseña eliminada → `404`.

---

## Bug 9 — Registro público de clientes sin autenticación 🟡

### Problema
`POST /api/auth/register` es un endpoint **público** que crea usuarios con rol `CUSTOMER` sin requerir autenticación. Cualquier persona en internet puede registrarse como cliente. El spec define:

> "El cliente es un usuario con cuenta preasignada. El supervisor crea la cuenta del cliente."

Esto significa que los clientes no se auto-registran: un supervisor o gerente crea su cuenta con contraseña preasignada y se la entrega.

### Solución
Hay dos opciones. La **recomendada** es proteger el endpoint para que solo supervisores y gerentes puedan crear clientes a través del endpoint ya existente (`POST /api/users/customers`), que ya está correctamente protegido.

#### Opción A (recomendada) — Deshabilitar el registro público
Reemplazar el contenido de `src/app/api/auth/register/route.ts` para que retorne `404` o `405`:

```ts
// src/app/api/auth/register/route.ts
import { NextResponse } from 'next/server'

// El registro público de clientes está deshabilitado.
// Los clientes son creados por supervisores o gerentes desde /api/users/customers
export async function POST() {
  return NextResponse.json(
    { error: 'El registro público no está habilitado. Contacte a su supervisor.' },
    { status: 403 }
  )
}
```

#### Opción B — Proteger el endpoint (si se decide mantener un registro propio)
Si el negocio decide mantener algún flujo de auto-registro en el futuro, el endpoint debería al menos:
1. Requerir un token de invitación de un solo uso generado por el supervisor.
2. O estar protegido con `requireAuth(['SUPERVISOR', 'MANAGER'])`.

Por ahora se recomienda la Opción A.

### Verificación
- `POST /api/auth/register` sin autenticación → `403 Forbidden`.
- `POST /api/users/customers` con token de SUPERVISOR → sigue funcionando correctamente.
- Verificar que el flujo de login para clientes preexistentes (creados por supervisores) no se vea afectado.

---

## Checklist de verificación post-corrección

| # | Prueba | Resultado esperado |
|---|--------|-------------------|
| 1a | CUSTOMER: `GET /api/reviews/{id}` — inspeccionar respuesta JSON | No contiene `internalNote` ni `actionTaken` |
| 1b | SUPERVISOR: `GET /api/reviews/{id}` | Contiene `internalNote` y `actionTaken` |
| 2a | SUPERVISOR responde reseña PENDING | Status cambia a `RESOLVED`, `resolvedAt` se pobla |
| 2b | Reseña resuelta aparece en dashboard del supervisor como "Resuelta" | ✅ |
| 3a | SUPERVISOR: `PUT /api/reviews/{id}/status` | Retorna `403 Forbidden` |
| 3b | MANAGER: `PUT /api/reviews/{id}/status` | Funciona correctamente |
| 4a | CUSTOMER A: `GET /api/reviews/{id_de_cliente_B}/photos` | Retorna `403 Forbidden` |
| 4b | CUSTOMER A: fotos de su propia reseña | Retorna `200` con las fotos |
| 5a | `POST /api/auth/register-push-token` con token válido | `200`, campo actualizado en BD |
| 5b | Sin body o token inválido | `400 Bad Request` |
| 6a | Cron con `Authorization: Bearer {CRON_SECRET}` y reseña +24h PENDING | Notificación creada para managers |
| 6b | Cron sin header de autorización | `401 Unauthorized` |
| 7a | Segunda reseña del mismo tipo+fecha | `409 Conflict` con código de reseña existente |
| 7b | Reseña misma fecha pero tipo diferente | `201 Created` |
| 8a | `DELETE` → verificar BD | Registro sigue en BD con `deletedAt` poblado |
| 8b | `GET /api/reviews` después del delete | Reseña no aparece en listado |
| 9a | `POST /api/auth/register` sin autenticación | `403 Forbidden` |
| 9b | Login de cliente preexistente | Funciona sin cambios |

---

## Notas para el desarrollador

- **Orden de implementación recomendado**: bugs 1 → 2 → 3 → 4 (críticos primero), luego 8 (requiere migración de BD), luego 7 → 9 → 5 → 6.
- El bug 8 requiere una migración de Prisma. Ejecutar `npx prisma migrate dev` y actualizar **todos** los queries que listen reseñas para incluir `deletedAt: null`.
- El bug 6 requiere que el server tenga acceso a `crontab`. Si se despliega en un entorno sin cron (serverless), considerar un servicio externo como [cron-job.org](https://cron-job.org) apuntando al endpoint con el header de autorización.
- Para la **segunda entrega (app móvil React Native + Expo)**: el campo `expoPushToken` ya está en el schema y el endpoint del bug 5 debe estar funcionando antes de iniciar esa entrega. Las notificaciones push reales (Expo Push Service) se integrarán en esa fase, modificando `src/lib/notifications.ts` para enviar también al servicio de Expo además de crear la notificación en BD.
