Meta rechaza el alta de WhatsApp (#2494070) cuando el nombre declarado
del negocio no coincide con el del sitio que lo respalda. El negocio es
Taller Beldor y el sitio decia Taller MG.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Las tres sesiones (admin, cliente, jefe) se firman con el mismo secreto, asi que
verificar solo la firma alcanzaba para que un token de un rol pasara por la
cookie de otro. Verificado antes del cambio: un token de admin en
cliente_session daba 200 en /api/cliente/autos, y uno de jefe cargaba
/cliente/dashboard. Despues: 401 y 307.
- Las 26 llamadas a verificarSesion pasan el rol esperado.
- El middleware, que hacia su propio jwtVerify sin mirar el rol, ahora tambien
lo comprueba.
- Se sacan las comprobaciones sueltas de rol en las rutas de jefe, que quedaron
redundantes.
Matriz probada contra la app: los tres roles siguen entrando en su area (200) y
ninguno entra en la de los otros, ni por API (401) ni por pagina (307).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
- verificarSesion acepta el rol esperado. Las tres sesiones (admin, cliente,
jefe) se firman con el mismo secreto, asi que un token de jefe puesto en la
cookie cliente_session pasaba la verificacion de firma; ahora el rol se
comprueba. Verificado: con un JWT de jefe, /api/cliente/perfil da 401.
- El update traduce el P2002 de los indices unicos a un 409 legible, por si otra
request toma el mismo email o telefono entre la comprobacion y el update.
- La pantalla ya no queda en "Cargando..." para siempre si falla el pedido.
- Si la sesion vence mientras edita, el PATCH manda a entrar de nuevo en vez de
mostrar un error suelto.
Queda como mejora aparte pasarle el rol esperado al resto de las rutas del area
del cliente, que hoy solo validan la firma.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
El cliente no tenia forma de corregir su email ni su telefono despues del alta.
Nueva pantalla /cliente/perfil ("Mis datos", enlazada desde el dashboard) que
muestra nombre, email y telefono, y permite editarlos.
- GET/PATCH /api/cliente/perfil, ambos con la sesion del cliente.
- El telefono usa el mismo campo con el +54 9 fijo que el registro, y se guarda
normalizado en telefonoNorm para que el bot lo siga reconociendo.
- aLocalArgentino() saca el prefijo para mostrar: un cliente dado de alta por
WhatsApp tiene el numero guardado como 5491141984844 y en el formulario tiene
que verse 1141984844.
- Email y telefono se validan unicos contra los demas clientes (409), no contra
uno mismo. Si cambia el email se avisa que con ese va a entrar de ahora en mas,
porque es la identidad del magic link.
- El middleware protege /cliente/perfil como el resto del area del cliente.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
- esLocalArgentinoValido tambien rechaza los que arrancan con 15: ninguna
caracteristica argentina empieza asi, es el viejo prefijo de celular.
- El formulario ya no reta "faltan digitos" mientras se esta tipeando; ese
aviso espera a que el campo pierda el foco. Los errores seguros (0 o 15
adelante) se siguen mostrando al instante.
- chat-bot.ts normalizaba el telefono para conversar pero no para /reset, asi
que el reset no encontraba la conversacion y no borraba nada.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Un cliente registrado por la web no lo reconocia el bot: el formulario dejaba
escribir el numero en cualquier formato (1141984844, 01157260394) mientras que
Meta manda el wa_id 5491141984844, asi que la busqueda por telefonoNorm no
encontraba nada, mandaba al alta y ahi chocaba contra el email ya registrado.
Quedaba en un callejon sin salida.
Ahora el campo muestra "+54 9" fijo y grisado y el cliente completa solo la
caracteristica (sin el 0) y el abonado (sin el 15), 10 digitos. El backend
valida ese formato y guarda telefonoNorm ya como lo espera el bot. Se rechaza
tambien un telefono repetido, que antes reventaba con el indice unico.
Se agrega scripts/chat-bot.ts, que es como se encontro esto: permite conversar
con el bot desde la consola contra cualquier base, sin Meta ni credenciales.
Corriendolo contra una copia de la base de produccion se ve el ciclo completo:
alta por web -> el bot lo reconoce por telefono -> pide el email -> reserva.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
La validacion rechaza 0 pero el mensaje decia "entre 0 y 24". Ojo que
"entre 1 y 24" habria sido igual de incorrecto: duracionHoras es Float y la
UI muestra las duraciones de menos de una hora en minutos, asi que 0,5 (media
hora) es un valor valido y esperado.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
- 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>
Los trabajos ahora son por jefe mecanico, asi que el cliente elige primero
el mecanico y despues el trabajo, igual que en el bot de WhatsApp. Antes
listaba todos los servicios del taller y recien despues pedia el jefe.
- src/lib/servicios.ts: regla unica de que trabajos ofrece un jefe = los
suyos + los globales del taller (jefeMecanicoId null, los carga el admin).
Asi no se rompe lo que ya existe en produccion, donde todos los servicios
son globales.
- /api/cliente/servicios ahora exige jefeMecanicoId y aplica esa regla.
- /api/cliente/slots y crearTurno rechazan un trabajo propio de OTRO jefe
(400): antes se podia reservar con el jefe A la duracion del trabajo del B.
- whatsapp-bot: usa la misma regla, asi la web y el bot ofrecen lo mismo.
- cliente/turnos: pasos invertidos, cabeceras y back acordes, y estado vacio
propio cuando el jefe no tiene trabajos.
- test-whatsapp-bot: soporte de OR en el Prisma en memoria, trabajo global en
el seed y escenario D (globales visibles, trabajo ajeno rechazado). 31 asserts.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Ultima fase del bot de reservas: conecta la maquina de conversacion ya
existente (whatsapp-bot.ts) con la WhatsApp Business Cloud API.
- src/lib/whatsapp.ts: traduce RespuestaBot -> payloads de Meta (texto e
interactive/button, con los limites de 3 botones / 20 chars / 4096 chars),
aplana el payload del webhook y valida X-Hub-Signature-256 en tiempo
constante.
- src/app/api/whatsapp/webhook/route.ts: GET de verificacion y POST de
mensajes. Valida la firma sobre el cuerpo crudo, deduplica por message id
(Meta reintenta) y siempre devuelve 200 para no entrar en loop de retries.
- scripts/test-whatsapp-envio.ts (npm run test:envio): 30 asserts sobre la
capa Meta con fetch stubbeado, sin DB ni credenciales.
- docs/whatsapp.md con el alta en el panel de Meta y las 4 env vars.
Falta solo cargar las credenciales de Meta en el .env de produccion.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
- lib/turnos.ts: crearTurno() compartido (valida servicio/jefe/slot); cliente/turnos POST lo reutiliza
- lib/telefono.ts: normalización de teléfono (E.164 sin '+') para matchear el wa_id
- lib/whatsapp-bot.ts: procesarMensaje(telefono, texto), state machine completa:
identidad por teléfono + verificación por email registrado / alta nueva,
elegir mecánico (botones) -> trabajo (letras) -> horario (letras, con "más") -> confirmar,
crea turno sin auto/km; expiración 30min, "cancelar" global, manejo de inválidos y 409.
Devuelve objetos-respuesta (texto/botones) -> independiente de Meta (lo traduce F0).
- scripts/test-whatsapp-bot.ts + npm run test:bot: corre la máquina de estados con un
Prisma en memoria (sin DB ni Meta), ejercitando scheduling y crearTurno reales. 27/27 OK.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
FASE 1 — schema:
- Cliente.email opcional; nuevo telefonoNorm (E.164, único) para matchear por WhatsApp
- TipoServicio.jefeMecanicoId opcional (trabajos per-jefe; globales legacy quedan null)
- Turno.autoId/kilometros opcionales (reservas por WhatsApp sin auto/km)
- Nuevo modelo ConversacionWhatsapp (estado del bot por teléfono)
- admin/turnos/[id]: no permite COMPLETADO sin auto+km; Sheets tolera email nulo
FASE 3 — panel de trabajos del jefe:
- API jefe/servicios (GET/POST) y jefe/servicios/[id] (PATCH/DELETE), scoped al jefe
- DELETE desactiva si el trabajo ya tiene turnos asociados
- Página /jefe/servicios (alta/listado/activar/editar/eliminar) + nav Turnos|Trabajos
- Dashboard del jefe resiliente a turnos sin auto/km ("a completar")
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Cliente now picks a jefe mecánico after choosing the service so the
calendar shows that specific jefe's availability. Each jefe has its own
configurable parallel slot capacity (replacing the global SLOTS_PARALELOS
= 5). Admins manage jefes from /admin/jefes; jefes log in to /jefe to
see their daily agenda.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- Background fijo con tallermg.jpeg + overlay rgba(0,0,0,0.35) en globals.css
- Favicon actualizado usando src/app/icon.jpeg (convención Next.js App Router)
- Eliminado bg-gray-50 de layouts para que el fondo sea visible
- Título del sitio actualizado a "Taller MG"
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Si alguien ingresaba un km fuera del rango integer de Postgres la app
crasheaba silenciosamente — el cliente no veía ningún mensaje. Se agrega
validación de rango (1–1.000.000) en frontend y backend, y se muestra
el error de la API debajo del botón de confirmar.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Muestra error inline si el cliente ingresa letras u otros caracteres,
y bloquea el botón de confirmar hasta que el valor sea válido.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Reemplaza lista de slots por calendario cuadrado mensual
- Sábados y domingos grisados, días sin slots deshabilitados
- Click en día hábil abre modal con horarios disponibles
- Slot dot indicator en días con turnos disponibles
- Amplía búsqueda a 60 días / 500 slots para cubrir todo el mes
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Reemplaza HistorialServicio por Turno en ambas APIs y frontends (cliente y admin).
Muestra: servicio, fecha, horario, estado (badge), km al ingreso, observaciones del jefe.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
El rango A:N confundía a la API cuando había datos manuales en K/L (GASTOS/VARIOS),
haciendo que la nueva fila empezara en columna K en vez de A.
INSERT_ROWS siempre inserta fila nueva independiente del estado del sheet.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
getHours() en Docker devuelve UTC → slots aparecían 3hs antes (5:30 en vez de 8:30).
Ahora usa getUTCHours/getUTCMinutes + offset -180min en todas las comparaciones.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Agrega campo comentario opcional al marcar turno COMPLETADO (textarea en UI, guardado en DB, escrito en col N del sheet)
- Corrige mapeo Google Sheets: email→I, telefono→J, vacíos en K-M, comentario→N
- Actualiza scheduling: inicio 8:30, fin 17:00, máx 5 turnos simultáneos
- entrypoint.sh: migrate deploy → db push para aplicar cambios de schema automáticamente
- Historial cliente: tarjetas visuales con header, fecha y observaciones destacadas
- Dashboard/turnos cliente: mejoras responsive mobile
- Fix display duración: 0.5 hs → "30 min"
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Cuando el admin marca un turno como COMPLETADO escribe una fila
en el sheet con patente, km, auto, cliente, fecha y trabajo.
Auth via service account con JWT nativo (sin googleapis).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Turno.kilometros (requerido al reservar)
- Auto.nroChasis (opcional)
- Combobox con 60 marcas mundiales y modelos precargados
- Modelo se habilita al seleccionar marca
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Reemplaza Resend por nodemailer con Gmail SMTP. El to: ahora usa
el email real del cliente en lugar del hardcodeado de sandbox.
Variables de entorno: GMAIL_USER y GMAIL_APP_PASSWORD.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Páginas cliente: login magic link, registro, validación de token,
dashboard de autos, historial por auto y reserva de turnos con selector
de servicio + slots disponibles.
Páginas admin: login, dashboard de turnos del día con cambio de estado,
historial de autos (buscar cliente → auto → ver/cargar registros),
gestión de tipos de servicio y vista de clientes.
Middleware JWT para proteger rutas /cliente/* y /admin/*.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Estructura inicial con Next.js 14, PostgreSQL/Prisma 7, autenticación por
magic link (clientes) y JWT (admins), lógica de scheduling con 4 slots
paralelos y API routes completas para cliente y panel admin.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>