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
+8
View File
@@ -138,6 +138,14 @@ const fakePrisma: any = {
return out;
},
},
// `crearTurno` verifica y crea dentro de una transacción con un lock por jefe.
// Acá corre todo sobre el mismo cliente falso, que ya es secuencial.
async $transaction(fn: any) {
return fn(fakePrisma);
},
async $executeRaw() {
return 0;
},
};
(globalThis as any).prisma = fakePrisma;
+31 -1
View File
@@ -32,6 +32,8 @@ function ok(cond: boolean, msg: string) {
// ── Stub de fetch: guarda lo que se le manda a Meta en vez de salir a internet ──
type Llamada = { url: string; auth: string; body: any };
let llamadas: Llamada[] = [];
// Códigos a devolver, en orden. Si se vacía, responde 200.
let respuestas: number[] = [];
globalThis.fetch = (async (url: string | URL, init?: RequestInit) => {
const headers = (init?.headers ?? {}) as Record<string, string>;
llamadas.push({
@@ -39,7 +41,8 @@ globalThis.fetch = (async (url: string | URL, init?: RequestInit) => {
auth: headers["Authorization"] ?? "",
body: JSON.parse(String(init?.body ?? "{}")),
});
return { ok: true, status: 200, text: async () => "" } as Response;
const status = respuestas.shift() ?? 200;
return { ok: status < 400, status, text: async () => "detalle" } as Response;
}) as typeof fetch;
async function main() {
@@ -97,6 +100,33 @@ async function main() {
ok(llamadas.length === 2, "manda las respuestas múltiples en orden");
ok(llamadas[0].body.text.body === "uno" && llamadas[1].body.text.body === "dos", "respeta el orden");
console.log("\n── Salida: reintentos ──");
llamadas = [];
respuestas = [429];
await enviarRespuestas("549351", [{ tipo: "texto", texto: "con rate limit" }]);
ok(llamadas.length === 2, "reintenta ante un 429 y sale bien en el segundo intento");
llamadas = [];
respuestas = [500, 503];
await enviarRespuestas("549351", [{ tipo: "texto", texto: "con 5xx" }]);
ok(llamadas.length === 3, "reintenta ante 5xx hasta que uno sale bien");
llamadas = [];
respuestas = [500, 500, 500];
await enviarRespuestas("549351", [{ tipo: "texto", texto: "siempre 5xx" }]);
ok(llamadas.length === 3, "no reintenta más de 3 veces");
llamadas = [];
respuestas = [400];
await enviarRespuestas("549351", [{ tipo: "texto", texto: "payload malo" }]);
ok(llamadas.length === 1, "un 400 no se reintenta (es error nuestro)");
llamadas = [];
respuestas = [401];
await enviarRespuestas("549351", [{ tipo: "texto", texto: "token vencido" }]);
ok(llamadas.length === 1, "un 401 no se reintenta");
respuestas = [];
console.log("\n── Firma del webhook ──");
const cuerpo = JSON.stringify({ hola: "chau", acento: "ñandú" });
const firma = "sha256=" + crypto.createHmac("sha256", "secreto-de-prueba").update(cuerpo, "utf8").digest("hex");