Plataforma de reserva y pago de citas
Agenda en línea con cobro anticipado: el paciente reserva y paga, y la franja se bloquea sola.
8
reservas simultáneas probadas
1
gana siempre
3
modalidades de atención
6
reglas de negocio aisladas
Una profesional independiente agendaba por WhatsApp y cobraba por transferencia. Perdía horas confirmando citas, sufría ausencias sin aviso y ocasionalmente prometía la misma hora a dos personas. Necesitaba que el sistema fuera dueño de la agenda y del cobro, con tres modalidades de atención y recargo por desplazamiento según el municipio.
API en .NET 10 con Clean Architecture y una aplicación React 19 en TypeScript. La profesional marca en un calendario qué días atiende y en qué horario; lo que no marca sencillamente no se ofrece. El paciente elige paquete, modalidad y hora, paga en línea, y la cita se confirma sola. Al confirmarse se dispara el correo y el evento de calendario con enlace de videollamada.
Que una franja no se venda dos veces no se puede garantizar desde el código de la aplicación: entre que se consulta la disponibilidad y se escribe la reserva, otro usuario puede colarse. La garantía real la da la base de datos con una restricción de exclusión sobre rangos de tiempo; el segundo intento simultáneo falla en el motor y la API lo traduce a un conflicto limpio. Hay una prueba que lanza ocho reservas a la vez sobre la misma franja y exige que gane exactamente una. El dinero, por su parte, solo lo confirma la pasarela: el webhook verifica su firma, registra el evento con clave única para tolerar reintentos, y vuelve a consultar la transacción al proveedor antes de confirmar nada.
Cero dobles reservas por diseño, no por suerte. Cobro anticipado que elimina las ausencias sin aviso, y una agenda que la profesional administra sola sin depender de nadie.
Arquitectura
Las líneas punteadas son flujos que ocurren solos, sin que nadie esté usando la aplicación.
Cómo se ve por dentro
Ejemplos ilustrativos de los patrones aplicados, con la razón de cada decisión.
La base de datos impide vender dos veces la misma hora
ALTER TABLE appointments
ADD CONSTRAINT ck_appointments_no_overlap
EXCLUDE USING gist (advisor_id WITH =, slot WITH &&)
WHERE (status IN ('PendingPayment', 'Confirmed'));Una restricción de exclusión sobre el rango horario. No hay ventana de carrera posible: el segundo insert simultáneo falla en el motor con el código 23P01, que la API traduce a un HTTP 409.
Ejemplo ilustrativo del patrón aplicado; no es código de ningún producto.Cada regla de negocio es una clase, no un if más
public interface IBookingRule
{
Task<RuleResult> EvaluateAsync(BookingRequest request, CancellationToken ct);
}
// Registro: añadir una regla no modifica ningún archivo existente
services.AddBookingRules(
typeof(AdvisorMustBeActiveRule),
typeof(SlotDurationMustMatchPackageRule),
typeof(SlotMustBeOfferedRule),
typeof(PatientMustNotHaveOverlappingAppointmentRule),
typeof(PatientPendingPaymentLimitRule));Patrón Strategy. Añadir una política nueva es crear una clase y registrarla; ni el orquestador ni los casos de uso se tocan. Es lo que mantiene el sistema barato de cambiar seis meses después.
Ejemplo ilustrativo del patrón aplicado; no es código de ningún producto.Stack del proyecto
¿Necesitas algo parecido?
Cuéntame tu caso y te digo con franqueza si es viable y cuánto toma.
Hablemos