fix: los cuatro hallazgos reales de la revision con IA
Revisión con IA / ai-review (pull_request) Successful in 29s

- crearTurno verificaba disponibilidad y creaba el turno en dos operaciones
  sueltas: dos clientes podian pasar la verificacion a la vez y sobrevender el
  mismo horario. Ahora va todo en una transaccion, serializada por jefe con
  pg_advisory_xact_lock. slotEstaDisponible acepta el cliente de la transaccion.
  Probado contra un Postgres real: 5 reservas simultaneas del mismo slot con
  capacidad 1 dan 1 ok + 4 conflictos y una sola fila; sacando el lock, entran
  las 5.
- La deduplicacion del webhook estaba en memoria y solo servia con una
  instancia. Pasa a la base con el modelo MensajeWhatsappProcesado: el insert
  del id es atomico por el indice unico, asi que aguanta reintentos
  concurrentes de Meta (verificado). Se limpian los ids de mas de 7 dias.
- postMensaje no reintentaba: ahora reintenta 3 veces ante 429, 5xx y fallas
  de red, con backoff exponencial y jitter. Un 4xx nuestro no se reintenta.
- /api/cliente/servicios devolvia los trabajos de un jefe dado de baja; ahora
  responde 404 si el jefe no existe o no esta activo.

test:envio suma los casos de reintento (35 asserts). El Prisma en memoria del
test del bot ahora entiende $transaction y $executeRaw.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Beldor
2026-08-05 11:21:35 -03:00
parent 44aeb58e77
commit 91112862a5
8 changed files with 168 additions and 44 deletions
+31 -14
View File
@@ -1,4 +1,6 @@
import { NextRequest, NextResponse } from "next/server";
import { Prisma } from "@prisma/client";
import { prisma } from "@/lib/prisma";
import { procesarMensaje } from "@/lib/whatsapp-bot";
import {
enviarRespuestas,
@@ -17,21 +19,34 @@ export const dynamic = "force-dynamic";
* POST → mensajes entrantes: se los pasa a la máquina de conversación y se contesta.
*/
// Meta reintenta el mismo mensaje si tardamos o si algo falla. Guardamos los ids ya
// procesados en memoria (un solo container) para no duplicar turnos ni respuestas.
const procesados = new Set<string>();
const MAX_PROCESADOS = 500;
// Meta reintenta el mismo mensaje si tardamos o si algo falla, y sin deduplicar eso
// crea el turno dos veces. Se resuelve insertando el id en la base: el índice único
// hace la operación atómica, así que sirve incluso con varias instancias corriendo.
const DIAS_RETENCION = 7;
let ultimaLimpieza = 0;
function yaProcesado(id: string): boolean {
if (procesados.has(id)) return true;
procesados.add(id);
// Set mantiene orden de inserción: soltamos los más viejos.
while (procesados.size > MAX_PROCESADOS) {
const viejo: string | undefined = procesados.values().next().value;
if (viejo === undefined) break;
procesados.delete(viejo);
async function yaProcesado(id: string): Promise<boolean> {
try {
await prisma.mensajeWhatsappProcesado.create({ data: { id } });
return false;
} catch (e) {
if (e instanceof Prisma.PrismaClientKnownRequestError && e.code === "P2002") return true;
throw e;
}
}
/** Borra los ids viejos como mucho una vez por día. Best-effort: si falla, no importa. */
async function limpiarProcesadosViejos(): Promise<void> {
const unDia = 24 * 60 * 60 * 1000;
if (Date.now() - ultimaLimpieza < unDia) return;
ultimaLimpieza = Date.now();
try {
await prisma.mensajeWhatsappProcesado.deleteMany({
where: { createdAt: { lt: new Date(Date.now() - DIAS_RETENCION * unDia) } },
});
} catch (e) {
console.error("[whatsapp] no se pudieron limpiar los mensajes procesados:", e);
}
return false;
}
export async function GET(req: NextRequest) {
@@ -64,10 +79,12 @@ export async function POST(req: NextRequest) {
}
try {
await limpiarProcesadosViejos();
for (const mensaje of mensajesDelPayload(body)) {
const telefono = mensaje.from;
const messageId = mensaje.id;
if (!telefono || !messageId || yaProcesado(messageId)) continue;
if (!telefono || !messageId || (await yaProcesado(messageId))) continue;
void marcarLeido(messageId).catch(() => {});