From 6e4cc7436df9dfad0585d45f9c812e777e4f8971 Mon Sep 17 00:00:00 2001 From: Lizandro Guarnizo <77708265+lizandrogd@users.noreply.github.com> Date: Thu, 18 Jun 2026 12:49:27 -0500 Subject: [PATCH] feat: send FCM push notification on service status change + update docs - services.module: import NotificationsModule - services.service: inject NotificationsService, notify the other party (user or professional) when service status changes to accepted/denied/ cancelled/active/completed - ARQUITECTURA.md: updated to reflect current migration state (all migrated) Co-Authored-By: Claude Sonnet 4.6 --- ARQUITECTURA.md | 136 +++++++++++++++++++++++ backend/src/services/services.module.ts | 3 +- backend/src/services/services.service.ts | 28 ++++- 3 files changed, 165 insertions(+), 2 deletions(-) create mode 100644 ARQUITECTURA.md diff --git a/ARQUITECTURA.md b/ARQUITECTURA.md new file mode 100644 index 0000000..7fa41d9 --- /dev/null +++ b/ARQUITECTURA.md @@ -0,0 +1,136 @@ +# ProsApp β€” Arquitectura del Proyecto + +``` +πŸ“ ~/Documents/GitHub/ +``` + +## Mapa de Componentes + +``` +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ USUARIOS (Web) β”‚ +β”‚ β”‚ +β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ +β”‚ β”‚ prosapp_landing_page β”‚ β”‚ prosappweb β”‚ β”‚ +β”‚ β”‚ (tambiΓ©n prosapp_2026)β”‚ β”‚ Flutter Web β”‚ β”‚ +β”‚ β”‚ HTML + Tailwind CDN β”‚ β”‚ Provider + REST API β”‚ β”‚ +β”‚ β”‚ βœ… COMPLETO β”‚ β”‚ βœ… MIGRADO β”‚ β”‚ +β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ +β”‚ β”‚ β”‚ β”‚ +β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ +β”‚ β”‚ prosapp-migration/admin β”‚ β”‚ +β”‚ β”‚ Next.js 16 + shadcn + Tailwind β”‚ β”‚ +β”‚ β”‚ Panel administrador β”‚ β”‚ +β”‚ β”‚ βœ… COMPLETO (95%) β”‚ β”‚ +β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + β”‚ + β”‚ HTTP / JWT + β”‚ +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ prosapp-migration/backend β”‚ +β”‚ NestJS + Prisma + PostgreSQL β”‚ +β”‚ βœ… COMPLETO β€” Deployado en Coolify β”‚ +β”‚ β”‚ +β”‚ MΓ³dulos: auth, users, professionals, services, professions, β”‚ +β”‚ locations, chat (Socket.IO), comments, β”‚ +β”‚ notifications (FCM), storage, settings β”‚ +β”‚ β”‚ +β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ +β”‚ β”‚ PostgreSQL β”‚ (servidor Coolify) β”‚ +β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + β”‚ + β”‚ HTTP / JWT + β”‚ +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ USUARIOS (Mobile) β”‚ +β”‚ β”‚ +β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ +β”‚ β”‚ prosappco β”‚ β”‚ +β”‚ β”‚ Flutter iOS/Android β”‚ β”‚ +β”‚ β”‚ Bloc + 8 repos packages β€” βœ… MIGRADO β”‚ β”‚ +β”‚ β”‚ β€’ user_repository β€’ professional_repository β”‚ β”‚ +β”‚ β”‚ β€’ chat_repository β€’ score_repository β”‚ β”‚ +β”‚ β”‚ β€’ city_repository β€’ service_repository β”‚ β”‚ +β”‚ β”‚ β€’ profession_repository β€’ setting_repository β”‚ β”‚ +β”‚ β”‚ Push notifications via FCM + firebase_messaging β”‚ β”‚ +β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ +``` + +--- + +## Estado por Proyecto + +| Carpeta | App | Tech | Estado | +|---|---|---|---| +| `prosapp-migration/backend` | API Central | NestJS + Prisma + PostgreSQL | βœ… **100%** β€” Deployado. Notificaciones FCM integradas en cambios de estado de servicio | +| `prosapp-migration/admin` | Panel Admin | Next.js 16 + shadcn | βœ… **95%** β€” Pendiente: test post-deploy | +| `prosappweb` | App Web (Flutter) | Flutter Web + Provider | βœ… **95%** β€” Migrado. Pendiente: testing manual | +| `prosappco` | App Mobile (iOS/Android) | Flutter + Bloc + 8 repos | βœ… **95%** β€” Migrado + FCM. Pendiente: testing manual | +| `prosapp_landing_page` | Landing Page | HTML + Tailwind CDN | βœ… **100%** β€” EstΓ‘tica, completa | + +--- + +## Lo que estΓ‘ hecho + +### Backend +- Auth: registro, login, JWT, perfil (`/auth/*`) +- Users: CRUD, foto, FCM token (`/users/*`) +- Professionals: perfil, horarios, aprobaciΓ³n (`/professionals/*`) +- Services: crear, listar (por rol), calendario, estado (`/services/*`) + - ValidaciΓ³n de transiciones de estado + - NotificaciΓ³n FCM automΓ‘tica al cambiar estado +- Comments/Scores: crear, listar por usuario/profesional, reputaciΓ³n +- Chat: Socket.IO + REST (`/chat/*`) +- Storage: upload a S3/local (`/storage/upload`) +- Settings: CRUD global +- Professions + Locations: catΓ‘logos +- Notifications: envΓ­o FCM vΓ­a `POST /notifications/send` + +### prosappweb (Flutter Web) +- Auth migrada: login, registro, `GET /auth/me` +- Todos los providers/models apuntan a REST API +- Rutas correctas: `/services/me`, `/services/professional`, `/services/professional/requests`, `/services/professional/calendar`, `/services/public-calendar/:id`, `/services/:id/status` +- DTOs correctos: snake_case, `rate`, `range1_hour1`, `location_preference` +- Status en inglΓ©s en todos los archivos +- Chat: `GET /chat/my`, `POST /chat/start/:id` +- Horario: `PATCH /professionals/me/schedules` con formato array +- Score: `POST /comments` con DTO correcto +- `user.proState` (int) en lugar del antiguo `user.state` (string espaΓ±ol) + +### prosappco (Flutter Mobile) +- 8 packages: `api_*_repository.dart` implementados (Firebase eliminado) +- `firebase_messaging` + `firebase_core` para push notifications +- FCM token registrado en backend al hacer login (`PATCH /users/me/fcm-token`) +- Notificaciones en foreground via `flutter_local_notifications` +- Background handler registrado +- Status conversion maps: int ↔ string +- `Timestamp` β†’ `String` en todas las entidades +- Professional UUID vs user_id: fallback resuelto en backend y Flutter + +--- + +## Pendiente (Fase 3 β€” Testing) + +| # | Tarea | QuiΓ©n | +|---|---|---| +| T1 | Testear login/registro en prosappweb y prosappco | Manual | +| T2 | Testear flujo completo: crear β†’ aceptar β†’ iniciar β†’ terminar β†’ puntuar | Manual | +| T3 | Verificar notificaciones push llegan al dispositivo | Manual | +| T4 | Probar chat en tiempo real (Socket.IO) | Manual | +| T5 | Verificar WebSockets en producciΓ³n | Manual | +| T6 | Probar CRUDs desde admin (Profesiones, Ciudades, Settings) | Manual | +| T7 | Verificar flujo aprobaciΓ³n/rechazo de profesionales desde admin | Manual | + +--- + +## Variables de Entorno Requeridas (Coolify) + +```env +DATABASE_URL=postgresql://... +JWT_SECRET=... +FCM_SERVER_KEY=... # Firebase Cloud Messaging server key +STORAGE_PATH=... # o S3 config si aplica +``` diff --git a/backend/src/services/services.module.ts b/backend/src/services/services.module.ts index 210d1b0..7a8b49c 100644 --- a/backend/src/services/services.module.ts +++ b/backend/src/services/services.module.ts @@ -2,9 +2,10 @@ import { Module } from '@nestjs/common'; import { ServicesService } from './services.service'; import { ServicesController } from './services.controller'; import { AuthModule } from '../auth/auth.module'; +import { NotificationsModule } from '../notifications/notifications.module'; @Module({ - imports: [AuthModule], + imports: [AuthModule, NotificationsModule], providers: [ServicesService], controllers: [ServicesController], exports: [ServicesService], diff --git a/backend/src/services/services.service.ts b/backend/src/services/services.service.ts index 120e9e8..2ddd08b 100644 --- a/backend/src/services/services.service.ts +++ b/backend/src/services/services.service.ts @@ -1,11 +1,21 @@ import { Injectable, NotFoundException, BadRequestException, ForbiddenException } from '@nestjs/common'; import { PrismaService } from '../prisma/prisma.service'; +import { NotificationsService } from '../notifications/notifications.service'; import { ServiceStatus, VALID_TRANSITIONS } from './dto/service.dto'; +const STATUS_MESSAGES: Partial> = { + [ServiceStatus.ACCEPTED]: { title: 'Servicio aceptado', body: 'El profesional aceptΓ³ tu solicitud.' }, + [ServiceStatus.DENIED]: { title: 'Servicio rechazado', body: 'El profesional rechazΓ³ tu solicitud.' }, + [ServiceStatus.CANCELLED]:{ title: 'Servicio cancelado', body: 'El servicio fue cancelado.' }, + [ServiceStatus.ACTIVE]: { title: 'Servicio iniciado', body: 'El profesional iniciΓ³ el servicio.' }, + [ServiceStatus.COMPLETED]:{ title: 'Servicio completado', body: 'Β‘El servicio ha finalizado! Puedes dejar tu puntuaciΓ³n.' }, +}; + @Injectable() export class ServicesService { constructor( private prisma: PrismaService, + private notifications: NotificationsService, ) {} async create(data: { @@ -74,10 +84,26 @@ export class ServicesService { } } - return this.prisma.services.update({ + const updated = await this.prisma.services.update({ where: { id: serviceId }, data: { status: newStatus as any }, }); + + // Notify the other party + const msg = STATUS_MESSAGES[newStatus]; + if (msg) { + const targetUserId = userId === service.user_id + ? (await this.prisma.professionals.findUnique({ where: { id: service.professional_id } }))?.user_id + : service.user_id; + if (targetUserId) { + const target = await this.prisma.users.findUnique({ where: { id: targetUserId }, select: { fcm_token: true } }); + if (target?.fcm_token) { + this.notifications.send(target.fcm_token, msg.title, msg.body).catch(() => {}); + } + } + } + + return updated; } async findByUser(userId: string, page = 1, limit = 20) {