Sistema de gestión de citas automatizado a través de WhatsApp Business API, construido con NestJS, TypeScript, PostgreSQL y siguiendo principios de Clean Architecture, DDD y CQRS.
- Reservaciones Automatizadas: Flujo conversacional completo vía WhatsApp
- Multi-Tenant: Soporte para múltiples negocios en una sola instancia
- CQRS Estricto: Separación completa entre comandos y queries
- Event-Driven: Arquitectura basada en eventos de dominio
- Optimistic Locking: Manejo de concurrencia con versioning de aggregates
- Property-Based Testing: Tests exhaustivos con fast-check
- Clean Architecture: Separación clara de capas (Domain, Application, Infrastructure, Presentation)
- Node.js: v18 o superior
- PostgreSQL: v14 o superior
- npm: v9 o superior
git clone https://github.com/cryptoganster/bookings-software.git
cd bookings-softwarenpm installCopiar el archivo de ejemplo y configurar las variables:
cp .env.example .envEditar .env con tus configuraciones:
# Application
NODE_ENV=development
PORT=3000
# Database
DB_HOST=localhost
DB_PORT=5432
DB_USERNAME=postgres
DB_PASSWORD=tu_password
DB_DATABASE=postgres_dev
# JWT
JWT_SECRET=tu_secret_key_muy_seguro
JWT_EXPIRATION=1d
# WhatsApp Business API (opcional para desarrollo)
WHATSAPP_API_URL=https://graph.facebook.com/v18.0/PHONE_NUMBER_ID
WHATSAPP_ACCESS_TOKEN=tu_token
WHATSAPP_WEBHOOK_VERIFY_TOKEN=tu_verify_token
# Logging
LOG_LEVEL=debug# Iniciar PostgreSQL con Docker Compose
docker-compose -f docker-compose.dev.yml up -d
# La base de datos estará disponible en localhost:5432Crear la base de datos manualmente:
psql -U postgres
CREATE DATABASE postgres_dev;
CREATE DATABASE postgres_test;
\q# Ejecutar migraciones
npm run migration:run
# Verificar estado de migraciones
npm run migration:show
# Ejecutar seeders (datos de prueba)
npm run seedLas migraciones gestionan el esquema de la base de datos de forma versionada.
# Generar migración automáticamente basada en cambios de entities
npm run migration:generate -- -n NombreDeLaMigracion
# Crear migración vacía para cambios manuales
npm run migration:create -- -n NombreDeLaMigracion# Ejecutar todas las migraciones pendientes
npm run migration:run
# Ver estado de migraciones
npm run migration:show
# Revertir última migración
npm run migration:revert# Ejecutar script de análisis para detectar problemas
npm run migration:analyzeEl script de análisis detecta:
- ✅ Migraciones con timestamps inválidos (deben ser 13 dígitos)
- ✅ Migraciones duplicadas
- ✅ Tablas faltantes en la base de datos
Las migraciones están organizadas por Bounded Context:
src/database/migrations/
├── 1702550000000-EnableUuidExtension.ts # Shared
├── 1702552000000-CreateUsersTable.ts # Auth BC
├── 1734480000000-RefactorUserRoles.ts # Auth BC
├── 1734481000000-StandardizeUsersTableNaming.ts # Auth BC
├── 1734482000000-CreateCustomersTable.ts # Customer BC
├── 1702553000000-CreateOfferingsTable.ts # Offering BC
├── 1734650000000-CreateSchedulesTable.ts # Availability BC
├── 1734650100000-CreateBlockoutsTable.ts # Availability BC
├── 1702551100000-CreateCapacitiesTable.ts # Availability BC
├── 1702551000000-CreateAppointmentsTable.ts # Booking BC
├── 1734650200000-CreateConversationsTable.ts # Conversation BC
├── 1734650300000-CreateMessagesTable.ts # Conversation BC
├── 1734650400000-CreateBusinessOwnersTable.ts # Account BC
├── 1734650500000-CreateBusinessesTable.ts # Business BC
└── 1766345899000-AddSearchIndexesToCustomers.ts # Customer BC
Documentación completa: apps/backend/src/database/MIGRATIONS.md
Los seeds poblan la base de datos con datos de prueba para desarrollo.
# Ejecutar todos los seeds en orden
npm run seed
# Los seeds se ejecutan en este orden:
# 1. auth (users)
# 2. account (business_owners)
# 3. business (businesses)
# 4. customer (customers)
# 5. offering (offerings)
# 6. availability (schedules, blockouts, capacities)
# 7. booking (appointments)
# 8. conversation (conversations, messages)| Tabla | Registros | Descripción |
|---|---|---|
| users | 2 | 1 BUSINESS_OWNER, 1 CUSTOMER |
| business_owners | 2 | 1 FREE plan, 1 PRO plan |
| businesses | 1 | Peluquería Central (activo) |
| customers | 25 | 12 anónimos, 8 registrados, 5 merged |
| offerings | 7 | 6 activos (15-90 min), 1 inactivo |
| schedules | 6 | Lun-Vie 9am-6pm, Sáb 10am-2pm, Dom cerrado |
| blockouts | 3 | Navidad, Año Nuevo, Vacaciones de verano |
| capacities | ~78 | 30 días de capacidad para cada offering |
| appointments | ~35 | 23 CONFIRMED, 5 CANCELLED, 7 COMPLETED |
| conversations | 8 | 3 ACTIVE, 2 AWAITING_ADMIN, 3 RESOLVED |
| messages | 29 | TEXT, BUTTON, LOCATION types |
Documentación completa: apps/backend/src/database/SEEDS.md
# Conectar a la base de datos
docker exec -it <container-id> psql -U postgres -d bookings-software
# Verificar conteo de registros
SELECT 'users' as table_name, COUNT(*) FROM users
UNION ALL
SELECT 'business_owners', COUNT(*) FROM business_owners
UNION ALL
SELECT 'businesses', COUNT(*) FROM businesses
UNION ALL
SELECT 'customers', COUNT(*) FROM customers
UNION ALL
SELECT 'offerings', COUNT(*) FROM offerings
UNION ALL
SELECT 'schedules', COUNT(*) FROM schedules
UNION ALL
SELECT 'blockouts', COUNT(*) FROM blockouts
UNION ALL
SELECT 'capacities', COUNT(*) FROM capacities
UNION ALL
SELECT 'appointments', COUNT(*) FROM appointments
UNION ALL
SELECT 'conversations', COUNT(*) FROM conversations
UNION ALL
SELECT 'messages', COUNT(*) FROM messages;# Verificar que PostgreSQL esté corriendo
docker-compose -f docker-compose.dev.yml ps
# Ver logs de PostgreSQL
docker-compose -f docker-compose.dev.yml logs postgres
# Reiniciar PostgreSQL
docker-compose -f docker-compose.dev.yml restart postgres# Revertir última migración
npm run migration:revert
# Volver a ejecutar
npm run migration:run
# Si persiste, verificar logs y estado de la base de datos
npm run migration:show# Los seeds usan TRUNCATE CASCADE, así que son seguros de re-ejecutar
npm run seed
# Si hay errores de foreign keys, verificar orden de ejecución en:
# apps/backend/src/database/seeds/seed.ts# Revertir todas las migraciones
npm run migration:revert
# Volver a ejecutar todo
npm run migration:run
npm run seed# Ejecutar tests de integridad de base de datos
npm run test -- database/__tests__
# Tests incluyen:
# - Validación de migraciones (timestamps, duplicados, tablas)
# - Validación de seeds (conteos, foreign keys, variedad de datos)# Iniciar con hot-reload
npm run start:dev
# La aplicación estará disponible en http://localhost:3000# Compilar TypeScript
npm run build
# Iniciar versión compilada
npm run start:prod# Iniciar con debugger
npm run start:debug# Ejecutar todos los tests unitarios
npm run test
# Ejecutar tests en modo watch
npm run test:watch
# Ejecutar tests con cobertura
npm run test:cov# Ejecutar tests end-to-end
npm run test:e2e
# Ejecutar test específico
npm run test:e2e -- conversation-flow.e2e-spec.tsLos tests de property-based están integrados en los tests unitarios y usan fast-check:
# Ejecutar tests que incluyen property-based tests
npm run test -- --testPathPattern="pbt.spec.ts"src/
├── shared/ # Shared Kernel
│ ├── kernel/ # Abstracciones base (VersionedAggregateRoot, ValueObject)
│ ├── vo/ # Value Objects compartidos (UUID, AggregateVersion)
│ └── infra/ # Implementaciones compartidas (UnitOfWork)
│
├── auth/ # Bounded Context: Autenticación
│ ├── domain/ # User aggregate, Events, Value Objects (UserRole enum)
│ ├── app/ # Commands (Register, Login, AddRole, RemoveRole), Queries, Event Handlers
│ ├── infra/ # Repositories, Mappers, Guards (JwtAuthGuard, RolesGuard), JWT Strategy
│ └── presentation/ # Controllers, DTOs (RegisterDto, AddUserRoleDto)
│
├── availability/ # Bounded Context: Disponibilidad
│ ├── domain/ # Capacity aggregate, Events
│ ├── app/ # Commands, Queries
│ ├── infra/ # Repositories, Factories
│ └── presentation/ # (vacío por ahora)
│
├── booking/ # Bounded Context: Reservaciones ⭐
│ ├── domain/ # Appointment aggregate, Events, Exceptions
│ ├── app/ # Commands, Queries, Event Handlers, Sagas
│ ├── infra/ # Repositories, Mappers
│ └── presentation/ # Controllers
│
└── conversation/ # Bounded Context: Conversaciones WhatsApp
├── domain/ # Conversation aggregate, Events
├── app/ # Commands, Queries
├── infra/ # WhatsApp Client
└── presentation/ # Webhook Controller
- Clean Architecture: Separación de capas con dependencias hacia el dominio
- Domain-Driven Design (DDD): Bounded Contexts, Aggregates, Value Objects, Domain Events
- CQRS: Separación estricta entre Commands (escritura) y Queries (lectura)
- Event-Driven: Comunicación entre Bounded Contexts vía Domain Events
- Optimistic Locking: Control de concurrencia con versioning
- Auth: Gestión de autenticación y autorización con roles múltiples (JWT)
- Soporte para roles:
BUSINESS_OWNER,CUSTOMER,ADMIN - Un usuario puede tener múltiples roles simultáneamente
- Gestión de roles: agregar/remover roles dinámicamente
- Verificación de email y activación/desactivación de cuentas
- Soporte para roles:
- Availability: Gestión de capacidad y horarios disponibles
- Booking: Gestión de citas y reservaciones (BC principal)
- Conversation: Integración con WhatsApp y flujo conversacional
- Repository Pattern: Abstracción de persistencia
- Unit of Work: Gestión de transacciones
- Factory Pattern: Creación de aggregates complejos
- Saga Pattern: Orquestación de procesos largos
- CQRS: CommandBus, QueryBus, EventBus de NestJS
npm run start:dev # Iniciar con hot-reload
npm run start:debug # Iniciar con debugger
npm run lint # Ejecutar ESLint
npm run format # Formatear con Prettiernpm run migration:generate # Generar migración
npm run migration:run # Ejecutar migraciones
npm run migration:revert # Revertir última migración
npm run migration:show # Ver estado de migraciones
npm run migration:analyze # Analizar migraciones (detectar problemas)
npm run seed # Ejecutar seedersnpm run test # Tests unitarios
npm run test:watch # Tests en modo watch
npm run test:cov # Tests con cobertura
npm run test:e2e # Tests end-to-endnpm run build # Compilar TypeScript
npm run start:prod # Iniciar versión compiladaPara agregar un nuevo Bounded Context siguiendo el patrón de Booking:
mkdir -p src/nuevo-bc/{domain,app,infra,presentation}
mkdir -p src/nuevo-bc/domain/{aggregates,events,vo,exceptions,interfaces}
mkdir -p src/nuevo-bc/app/{commands,queries,event-handlers,sagas}
mkdir -p src/nuevo-bc/infra/{persistence,external}
mkdir -p src/nuevo-bc/presentation/controllers// src/nuevo-bc/domain/aggregates/mi-aggregate.ts
import { VersionedAggregateRoot } from "@shared/kernel/versioned-aggregate-root";
import { UUID } from "@shared/vo/uuid";
export class MiAggregate extends VersionedAggregateRoot {
private id: UUID;
static create(id: UUID, ...params): MiAggregate {
const aggregate = new MiAggregate();
aggregate.id = id;
aggregate.incrementVersion();
aggregate.apply(new MiAggregateCreated(id.getValue()));
return aggregate;
}
// Métodos de negocio...
}// src/nuevo-bc/app/commands/mi-command/command.ts
import { Command } from "@nestjs/cqrs";
export class MiCommand extends Command<{ id: string }> {
constructor(public readonly param: string) {
super();
}
}
// src/nuevo-bc/app/commands/mi-command/handler.ts
import { CommandHandler, ICommandHandler } from "@nestjs/cqrs";
@CommandHandler(MiCommand)
export class MiCommandHandler implements ICommandHandler<MiCommand> {
async execute(command: MiCommand): Promise<{ id: string }> {
// Implementación...
}
}// src/nuevo-bc/nuevo-bc.module.ts
import { Module } from "@nestjs/common";
import { CqrsModule } from "@nestjs/cqrs";
@Module({
imports: [CqrsModule],
providers: [
// Command Handlers
MiCommandHandler,
// Query Handlers
// Event Handlers
// Repositories
],
exports: [],
})
export class NuevoBcModule {}// src/app.module.ts
import { NuevoBcModule } from "./nuevo-bc/nuevo-bc.module";
@Module({
imports: [
// ...otros módulos
NuevoBcModule,
],
})
export class AppModule {}Verificar que PostgreSQL esté corriendo y las credenciales sean correctas:
# Verificar estado de PostgreSQL
docker-compose -f docker-compose.dev.yml ps
# Ver logs de PostgreSQL
docker-compose -f docker-compose.dev.yml logs postgresRevertir migraciones y volver a ejecutar:
npm run migration:revert
npm run migration:runLimpiar base de datos de test y volver a ejecutar:
npm run test:e2ePara más detalles sobre troubleshooting de base de datos, ver la sección Database Management arriba.
El proyecto implementa un pipeline completo de CI/CD con seguridad integrada:
El pipeline de CI se ejecuta automáticamente en cada push y pull request:
- ✅ Linting: ESLint en backend y frontend
- ✅ Formatting: Prettier check
- ✅ Type Checking: TypeScript compilation
- ✅ Security Audit: npm audit para vulnerabilidades
- ✅ License Check: Verificación de licencias compatibles
- ✅ Secret Scanning: TruffleHog para detectar secretos expuestos
- ✅ Tests: Jest (backend) y Vitest (frontend) con cobertura
- ✅ Build: Compilación de backend y frontend
- ✅ CodeQL: Análisis estático de seguridad (SAST)
- CodeQL: Análisis estático de código (SAST)
- Dependabot: Actualizaciones automáticas de dependencias
- Secret Scanning: Detección de secretos en código
- npm audit: Escaneo de vulnerabilidades en dependencias
- TruffleHog: Escaneo de secretos en historial de git
# Instalar act (GitHub Actions local runner)
brew install act # macOS
# o
curl https://raw.githubusercontent.com/nektos/act/master/install.sh | sudo bash
# Ejecutar pipeline de CI
act push
# Ejecutar job específico
act push -j lint- Setup Guide - Configuración de GitHub
- Workflows README - Documentación de workflows
- Secrets Management - Gestión de secretos
- Verification Checklist - Checklist de validación
- Fork el proyecto
- Crear una rama para tu feature (
git checkout -b feature/amazing-feature) - Commit tus cambios (
git commit -m 'feat: add amazing feature') - Push a la rama (
git push origin feature/amazing-feature) - Abrir un Pull Request
Este proyecto está bajo la Licencia MIT. Ver el archivo LICENSE para más detalles.
- Bryan Stevens - Desarrollo Inicial - cryptoganster
El sistema implementa un modelo de autenticación basado en roles múltiples, donde un usuario puede tener varios roles simultáneamente:
BUSINESS_OWNER: Dueño de negocio, puede administrar servicios, horarios y ver citasCUSTOMER: Cliente que agenda citas (futuro: panel web para clientes)ADMIN: Administrador del sistema con permisos completos
POST /api/auth/register
Content-Type: application/json
{
"email": "usuario@example.com",
"password": "password123",
"name": "Juan Pérez",
"initialRole": "BUSINESS_OWNER" // Opcional, default: BUSINESS_OWNER
}Respuesta:
{
"user": {
"id": "uuid",
"email": "usuario@example.com",
"name": "Juan Pérez",
"roles": ["BUSINESS_OWNER"],
"isActive": true,
"emailVerified": false
},
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
}POST /api/auth/login
Content-Type: application/json
{
"email": "usuario@example.com",
"password": "password123"
}Respuesta:
{
"user": {
"id": "uuid",
"email": "usuario@example.com",
"name": "Juan Pérez",
"roles": ["BUSINESS_OWNER", "CUSTOMER"],
"isActive": true,
"emailVerified": true
},
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
}El token JWT contiene:
{
"userId": "uuid",
"email": "usuario@example.com",
"roles": ["BUSINESS_OWNER", "CUSTOMER"],
"iat": 1734480000,
"exp": 1734566400
}Nota: El JWT NO contiene businessId. Para obtener el negocio de un usuario, usar el endpoint correspondiente del Business BC.
POST /api/auth/users/:userId/roles
Authorization: Bearer {token}
Content-Type: application/json
{
"role": "CUSTOMER"
}Reglas:
- No se puede agregar un rol que el usuario ya tiene
- El usuario debe existir y estar activo
DELETE /api/auth/users/:userId/roles/:role
Authorization: Bearer {token}Reglas:
- No se puede remover el último rol de un usuario
- El usuario siempre debe tener al menos un rol
import { Controller, Get, UseGuards } from "@nestjs/common";
import { JwtAuthGuard } from "@auth/infra/guards/jwt-auth.guard";
import { RolesGuard } from "@auth/infra/guards/roles.guard";
import { Roles } from "@auth/presentation/decorators/roles.decorator";
import { UserRole } from "@auth/domain/vo/user-role";
@Controller("admin")
@UseGuards(JwtAuthGuard, RolesGuard)
export class AdminController {
@Get("dashboard")
@Roles(UserRole.ADMIN)
getDashboard() {
// Solo accesible para usuarios con rol ADMIN
return { message: "Admin dashboard" };
}
@Get("business-stats")
@Roles(UserRole.BUSINESS_OWNER, UserRole.ADMIN)
getBusinessStats() {
// Accesible para BUSINESS_OWNER o ADMIN
return { message: "Business statistics" };
}
}import { Controller, Get, UseGuards } from "@nestjs/common";
import { JwtAuthGuard } from "@auth/infra/guards/jwt-auth.guard";
import { CurrentUser } from "@auth/presentation/decorators/current-user.decorator";
import { UserPayload } from "@auth/presentation/decorators/current-user.decorator";
@Controller("profile")
@UseGuards(JwtAuthGuard)
export class ProfileController {
@Get()
getProfile(@CurrentUser() user: UserPayload) {
// user contiene: { userId, email, roles }
return {
id: user.userId,
email: user.email,
roles: user.roles,
};
}
}POST /api/auth/verify-email
Authorization: Bearer {token}
Content-Type: application/json
{
"userId": "uuid"
}Reglas:
- Solo se puede verificar una vez
- Intentar verificar un email ya verificado lanza
EmailAlreadyVerifiedException
POST /api/auth/users/:userId/deactivate
Authorization: Bearer {token}POST /api/auth/users/:userId/activate
Authorization: Bearer {token}Reglas:
- Usuarios desactivados no pueden hacer login
- Las operaciones son idempotentes (no fallan si ya están en ese estado)
Cuando un usuario se registra con rol BUSINESS_OWNER, el Account BC automáticamente:
- Escucha el evento
UserRegistered - Crea un
BusinessOwnervinculado al usuario - Asigna plan de suscripción inicial (FREE)
Cuando un cliente anónimo se vincula a un usuario:
- Customer BC publica evento
CustomerLinkedToUser - Auth BC escucha el evento
- Agrega automáticamente el rol
CUSTOMERal usuario
El sistema sigue una arquitectura unificada de identidades:
User (Auth BC) → Identidad Universal
↓ ↓
BusinessOwner (Account) Customer (Customer)
↓ ↓
Business (Business) Appointment (Booking)
Beneficios:
- Un usuario puede ser proveedor (BUSINESS_OWNER) y consumidor (CUSTOMER) simultáneamente
- Preparado para marketplace: Juan (abogado) publica servicios Y agenda cita con dentista
- Separación clara de concerns: User = autenticación, BusinessOwner = cuenta, Business = negocio
Para más detalles, ver: .kiro/steering/user-customer-businessowner-architecture.md
El sistema emite eventos en tiempo real vía WebSocket para notificar cambios a los clientes conectados.
import { io } from "socket.io-client";
const socket = io("http://localhost:3000", {
auth: {
token: "your-jwt-token",
},
});
// El servidor automáticamente une al cliente a la room de su negocio
// Room: business:{businessId}Emitido cuando se crea un nuevo servicio.
Payload:
{
offeringId: string; // UUID del offering
name: string; // Nombre del servicio
durationMinutes: number; // Duración en minutos
maxCapacityPerSlot: number; // Capacidad máxima por slot
maxDailyCapacity: number | null; // Límite diario (opcional)
timestamp: string; // ISO 8601 timestamp
}Ejemplo:
socket.on("offering:created", (data) => {
console.log("Nuevo servicio creado:", data);
// Actualizar UI, invalidar cache, etc.
});Emitido cuando se actualiza un servicio existente.
Payload:
{
offeringId: string; // UUID del offering
name: string; // Nombre actualizado
durationMinutes: number; // Duración actualizada
maxCapacityPerSlot: number; // Capacidad actualizada
maxDailyCapacity: number | null; // Límite diario actualizado
timestamp: string; // ISO 8601 timestamp
}Ejemplo:
socket.on("offering:updated", (data) => {
console.log("Servicio actualizado:", data);
// Actualizar servicio en UI
});Emitido cuando se desactiva un servicio.
Payload:
{
offeringId: string; // UUID del offering desactivado
timestamp: string; // ISO 8601 timestamp
}Ejemplo:
socket.on("offering:deactivated", (data) => {
console.log("Servicio desactivado:", data);
// Marcar servicio como inactivo en UI
});Emitido cuando se reactiva un servicio previamente desactivado.
Payload:
{
offeringId: string; // UUID del offering activado
timestamp: string; // ISO 8601 timestamp
}Ejemplo:
socket.on("offering:activated", (data) => {
console.log("Servicio activado:", data);
// Marcar servicio como activo en UI
});Emitido cuando se crea una nueva cita.
Payload:
{
appointmentId: string; // UUID de la cita
customerId: string; // UUID del cliente
offeringId: string; // UUID del servicio
dateTime: string; // ISO 8601 timestamp de la cita
timestamp: string; // ISO 8601 timestamp del evento
}Emitido cuando se cancela una cita.
Payload:
{
appointmentId: string; // UUID de la cita cancelada
timestamp: string; // ISO 8601 timestamp
}Nota: Este evento se broadcast a todos los clientes conectados (no solo al negocio) debido a limitaciones del evento de dominio. Los clientes deben filtrar por appointmentId.
Emitido cuando se modifica una cita existente.
Payload:
{
appointmentId: string; // UUID de la cita
newDateTime: string; // Nueva fecha/hora (ISO 8601)
timestamp: string; // ISO 8601 timestamp
}Nota: Este evento se broadcast a todos los clientes conectados (no solo al negocio) debido a limitaciones del evento de dominio. Los clientes deben filtrar por appointmentId.
Los eventos de Offering se emiten solo a los clientes del mismo negocio (room business:{businessId}), garantizando aislamiento de datos entre tenants.
Los eventos de Appointment actualmente se emiten a todos los clientes debido a limitaciones en los eventos de dominio (no incluyen businessId). Esto será mejorado en versiones futuras.
socket.on("connect_error", (error) => {
console.error("Error de conexión:", error);
});
socket.on("error", (error) => {
console.error("Error de WebSocket:", error);
});import { io } from "socket.io-client";
const socket = io("http://localhost:3000", {
auth: {
token: localStorage.getItem("jwt-token"),
},
});
// Escuchar eventos de offerings
socket.on("offering:created", (data) => {
// Agregar nuevo offering a la lista
addOfferingToUI(data);
});
socket.on("offering:updated", (data) => {
// Actualizar offering en la lista
updateOfferingInUI(data);
});
socket.on("offering:deactivated", (data) => {
// Marcar como inactivo
markOfferingAsInactive(data.offeringId);
});
socket.on("offering:activated", (data) => {
// Marcar como activo
markOfferingAsActive(data.offeringId);
});
// Escuchar eventos de appointments
socket.on("appointment:created", (data) => {
// Agregar nueva cita al calendario
addAppointmentToCalendar(data);
});
socket.on("appointment:cancelled", (data) => {
// Remover cita del calendario
removeAppointmentFromCalendar(data.appointmentId);
});
socket.on("appointment:modified", (data) => {
// Actualizar cita en el calendario
updateAppointmentInCalendar(data);
});
// Manejo de errores
socket.on("connect_error", (error) => {
console.error("Error de conexión:", error);
showErrorNotification("No se pudo conectar al servidor");
});- NestJS por el excelente framework
- La comunidad de DDD y CQRS por los patrones y mejores prácticas
- fast-check por la librería de property-based testing