91112862a5
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>
71 lines
2.5 KiB
TypeScript
71 lines
2.5 KiB
TypeScript
import { prisma } from "./prisma";
|
|
import { slotEstaDisponible } from "./scheduling";
|
|
import { trabajoEsDeJefe } from "./servicios";
|
|
|
|
export interface CrearTurnoInput {
|
|
clienteId: string;
|
|
jefeMecanicoId: string;
|
|
tipoServicioId: string;
|
|
fechaInicio: Date;
|
|
autoId?: string | null;
|
|
kilometros?: number | null;
|
|
}
|
|
|
|
export type CrearTurnoResultado =
|
|
| { ok: true; turno: Awaited<ReturnType<typeof prisma.turno.create>> }
|
|
| { ok: false; error: string; status: number };
|
|
|
|
/**
|
|
* Crea un turno validando servicio/jefe activos y disponibilidad del slot.
|
|
* Lo usan tanto el flujo web (con auto+km) como el bot de WhatsApp (sin auto+km).
|
|
* La duración del turno la define el TipoServicio.
|
|
*/
|
|
export async function crearTurno(input: CrearTurnoInput): Promise<CrearTurnoResultado> {
|
|
const { clienteId, jefeMecanicoId, tipoServicioId, fechaInicio } = input;
|
|
|
|
const servicio = await prisma.tipoServicio.findUnique({ where: { id: tipoServicioId } });
|
|
if (!servicio || !servicio.activo)
|
|
return { ok: false, error: "Servicio no disponible", status: 404 };
|
|
|
|
const jefe = await prisma.jefeMecanico.findUnique({ where: { id: jefeMecanicoId } });
|
|
if (!jefe || !jefe.activo)
|
|
return { ok: false, error: "Jefe mecánico no disponible", status: 404 };
|
|
|
|
// Un trabajo propio de otro jefe no se reserva con éste (los globales sí).
|
|
if (!trabajoEsDeJefe(servicio, jefeMecanicoId))
|
|
return { ok: false, error: "Ese trabajo no lo hace este mecánico", status: 400 };
|
|
|
|
const fin = new Date(fechaInicio.getTime() + servicio.duracionHoras * 60 * 60 * 1000);
|
|
|
|
// Verificar y crear en una sola transacción, serializando por jefe con un lock
|
|
// de Postgres: si no, dos clientes pueden pasar la verificación al mismo tiempo
|
|
// y quedar los dos reservados sobre la misma capacidad.
|
|
const turno = await prisma.$transaction(async (tx) => {
|
|
await tx.$executeRaw`SELECT pg_advisory_xact_lock(hashtext(${jefeMecanicoId}))`;
|
|
|
|
const disponible = await slotEstaDisponible(fechaInicio, fin, jefeMecanicoId, tx);
|
|
if (!disponible) return null;
|
|
|
|
return tx.turno.create({
|
|
data: {
|
|
clienteId,
|
|
jefeMecanicoId,
|
|
tipoServicioId,
|
|
fechaInicio,
|
|
fechaFin: fin,
|
|
autoId: input.autoId ?? null,
|
|
kilometros: input.kilometros ?? null,
|
|
},
|
|
include: {
|
|
auto: true,
|
|
tipoServicio: true,
|
|
jefeMecanico: { select: { id: true, nombre: true } },
|
|
},
|
|
});
|
|
});
|
|
|
|
if (!turno) return { ok: false, error: "El horario ya no está disponible", status: 409 };
|
|
|
|
return { ok: true, turno };
|
|
}
|