diff --git a/README.md b/README.md index 711a702..a99f15a 100644 --- a/README.md +++ b/README.md @@ -1,295 +1,390 @@ # Notification-Service -Ce projet implémente un **service de notifications** en **Node.js**, **Express** et **TypeScript**. -Il gère deux fonctionnalités principales : +Service de notifications (SMS, email, OTP) base sur Node.js, Express, TypeScript, PostgreSQL, RabbitMQ. -- La génération et la vérification d’OTP (codes à usage unique). -- L’envoi de notifications (par e-mail,SMS ou autres canaux). +## Vue d'ensemble ---- +Ce service expose des endpoints HTTP et consomme des evenements RabbitMQ pour envoyer des notifications. -## Fonctionnalités principales +Comportement cle actuel: -- Génération et validation d’OTP avec expiration automatique. -- Envoi de notifications personnalisées via des templates. -- Architecture modulaire : contrôleurs, services, entités, utilitaires. +- OTP generation: telephone only, canal SMS. +- Alertes securite: SMS prioritaire, email secondaire si disponible. +- Les types de notification sont centralises dans `TypeNotification`. ---- +## Prerequis -# Endpoints +- Node.js >= 18 +- npm +- PostgreSQL +- RabbitMQ +- Compte Twilio (SMS) +- Compte SMTP/Gmail (email) -Tous les endpoints sont accessibles sous :
-/api/notifications +## Installation -## Fonctionnalités principales +```bash +cd notification_service +npm install +npm run build +npm run dev +``` + +## Variables d'environnement + +### API + +- SERVICE_PORT (ex: 8000) +- SERVICE_VERSION (optionnel) +- COMMIT_SHA (optionnel) + +### PostgreSQL + +- DB_HOST +- DB_PORT (defaut 5432) +- DB_USER +- DB_PASSWORD +- DB_NAME + +### RabbitMQ + +- RABBITMQ_URL +- RABBITMQ_EXCHANGE +- RABBITMQ_QUEUE + +### SMS (Twilio) + +- TWILIO_ACCOUNT_SID +- TWILIO_AUTH_TOKEN +- TWILIO_PHONE_NUMBER + +### Email + +- MAIL_USER +- MAIL_PASS + +### Health + +- HEALTH_CHECK_TIMEOUT_MS (defaut 1000) +- HEALTH_CACHE_TTL_MS (defaut 5000) +- HEALTH_EXPOSE_ERRORS (defaut false) + +## Commandes -- Génération et validation d’OTP avec expiration automatique. -- Envoi de notifications personnalisées via des templates. -- Intégration RabbitMQ : consommation d’événements de `wallet-service` (dépôt, retrait, transfert, OTP…) et transformation en notifications. -- Validation stricte des payloads HTTP avec **Zod** (emails et téléphones obligatoires, structure `transfer` dédiée, etc.). +```bash +npm run dev +npm run build +npm start +``` + +## Base URL + +```text +http://{host}:{SERVICE_PORT} +``` ---- +## Endpoints -## Endpoints HTTP +### 1) Health liveness -Tous les endpoints HTTP exposés par ce service sont préfixés par : +- Methode: GET +- Route: /health -- `/api/notifications` +Reponse exemple: -### 1. Envoi d’une notification (HTTP direct) +```json +{ + "status": "OK", + "uptime": 123.45 +} +``` -`POST /api/notifications/envoyer` +### 2) Health readiness -Depuis la refonte, le service est **strictement dépendant des coordonnées fournies dans le JSON**. Deux formes sont possibles : +- Methode: GET +- Route: /health/ready -#### a) Notification de transfert +Reponse exemple: + +```json +{ + "status": "OK", + "uptime": 123.45, + "timestamp": "2026-03-24T10:00:00.000Z", + "version": "1.0.0", + "commit": "abc123", + "components": { + "db": { "status": "OK" }, + "rabbitmq": { "status": "OK" } + } +} +``` + +### 3) Envoi notification HTTP directe + +- Methode: POST +- Route: /api/notifications/envoyer + +#### Cas A: transfer (sender/receiver avec email + phone obligatoires) ```json { "type": "transfer", "sender": { - "email": "expediteur@mail.com", - "phone": "+22300000000" + "email": "sender@example.com", + "phone": "+22370000001" }, "receiver": { - "email": "destinataire@mail.com", - "phone": "+22311111111" + "email": "receiver@example.com", + "phone": "+22370000002" }, "amount": 5000, - "content": "Transfert de 5000 FCFA réussi." + "content": "Transfert de 5000 FCFA effectue" +} +``` + +#### Cas B: alert_securite (SMS prioritaire, email optionnel) + +```json +{ + "type": "alert_securite", + "user": { + "phone": "+22370000003", + "email": "client@example.com" + }, + "content": "Tentative de connexion suspecte detectee" +} +``` + +#### Cas C: autre type simple (schema standard) + +Note: pour les types simples hors transfer et alert_securite, `user.email` et `user.phone` sont attendus. + +```json +{ + "type": "CLIENT_COMPTE_ACTIF", + "user": { + "email": "client@example.com", + "phone": "+22370000004" + }, + "content": "Votre compte est desormais actif" +} +``` + +### 4) Liste des notifications + +- Methode: GET +- Route: /api/notifications + +Reponse exemple: + +```json +[ + { + "id": "0f4c...", + "utilisateurId": "user-123", + "typeNotification": "ALERT_SECURITE", + "canal": "SMS", + "message": "Alerte securite...", + "statut": "ENVOYEE", + "dateEnvoi": "2026-03-24T10:00:00.000Z" + } +] +``` + +### 5) Publication test RabbitMQ + +- Methode: POST +- Route: /api/notifications/rabbitmq + +Request exemple: + +```json +{ + "routingKey": "notification.process", + "message": { + "utilisateurId": "user-123", + "typeNotification": "ALERT_SECURITE", + "canal": "SMS", + "phone": "+22370000005" + } +} +``` + +Reponse exemple: + +```json +{ + "success": true +} +``` + +### 6) OTP generate + +- Methode: POST +- Route: /api/notifications/otp/generate +- Regle actuelle: telephone only, SMS only. + +Request exemple minimal: + +```json +{ + "phone": "+22370000006" } ``` -- Le schéma Zod impose : - - `type` = `"transfer"`. - - `sender.email` / `sender.phone` obligatoires. +Request exemple avec utilisateurId: + +```json +{ + "utilisateurId": "pre-user-001", + "phone": "+22370000006" +} +``` + +Reponse exemple: + +```json +{ + "success": true, + "message": "OTP envoye", + "expiration": "2026-03-24T10:05:00.000Z" +} +``` + +### 7) OTP verify + +- Methode: POST +- Route: /api/notifications/otp/verify + +Request exemple: + +```json +{ + "utilisateurId": "pre-user-001", + "code": "1234" +} +``` + +Reponse exemple: + +```json +{ + "success": true, + "message": "OTP valide" +} +``` + +## RabbitMQ inter-services + +Message type attendu pour notification inter-service: + +```json +{ + "utilisateurId": "user-123", + "typeNotification": "ALERT_SECURITE", + "canal": "SMS", + "email": "client@example.com", + "phone": "+22370000007", + "context": { + "reason": "multiple_failed_pin_attempts" + }, + "metadata": { + "service": "wallet-service", + "correlationId": "evt-123" + } +} +``` + +Regle importante: + +- Pour `ALERT_SECURITE`, le service applique une priorite SMS quand un numero est present, puis envoi email si adresse disponible. + +## Types de notification disponibles + +### 1. Gestion admin + +- ADMIN_CREE +- ADMIN_MIS_A_JOUR +- ADMIN_SUPPRIME + +### 2. Agent + +- AGENT_INSCRIPTION +- AGENT_EN_ATTENTE_VALIDATION +- AGENT_VALIDE +- AGENT_REJETE + +### 3. Client + +- CLIENT_INSCRIPTION +- CLIENT_COMPTE_ACTIF + +### 4. Authentification et securite + +- CONNEXION_REUSSIE +- ECHEC_CONNEXION +- DECONNEXION +- NOUVEL_APPAREIL +- CHANGEMENT_MOT_DE_PASSE +- CHANGEMENT_EMAIL +- CHANGEMENT_TELEPHONE +- COMPTE_BLOQUE +- COMPTE_DEBLOQUE +- ALERT_SECURITE - # Notification-Service +### 5. Transactions - Service de notifications (e-mail & SMS & OTP) développé en Node.js, Express et TypeScript. +- CONFIRMATION_TRANSFERT +- CONFIRMATION_DEPOT +- CONFIRMATION_RETRAIT +- TRANSFERT_ENVOYE +- TRANSFERT_RECU +- ECHEC_TRANSFERT +- DEPOT_EN_COURS +- DEPOT_REUSSI +- ECHEC_DEPOT +- RETRAIT_EN_COURS +- RETRAIT_REUSSI +- ECHEC_RETRAIT - Ce README décrit l'installation, la configuration, les endpoints, les variables d'environnement et les bonnes pratiques pour déployer et tester le service. +### 6. OTP et verification - Table des matières - - Présentation - - Prérequis - - Installation - - Variables d'environnement - - Commandes utiles - - Endpoints et exemples - - Health checks - - Docker / Compose - - Débogage et logs - - Notes de sécurité +- OTP_ENVOYE +- OTP_VALIDE +- OTP_EXPIRE +- OTP_INVALIDE +- VERIFICATION_EMAIL +- VERIFICATION_TELEPHONE - *** +### 7. KYC - ## Présentation +- KYC_EN_COURS +- KYC_VALIDE +- KYC_REJETE +- VERIFICATION_KYC - Ce service reçoit des requêtes HTTP pour envoyer des notifications et générer/vérifier des OTP. Il s'intègre avec : - - PostgreSQL (TypeORM) - - RabbitMQ (échange partagé, queue privée) - - Twilio (SMS) - - Nodemailer (e-mail) +### 8. Paiement - Le code organise les responsabilités en contrôleurs, services, entités, utilitaires et messaging (publisher/consumer). +- PAIEMENT_REUSSI +- PAIEMENT_ECHOUE +- FACTURE_GENEREE +- FACTURE_PAYEE - ## Prérequis - - Node.js >= 18 - - npm - - PostgreSQL accessible (ou instance locale) - - RabbitMQ accessible (ou instance locale) - - Compte Twilio (si SMS en production) ou configuration de mock - - Compte e-mail (Gmail ou SMTP compatible) pour envoi d'e-mails +### 9. Fraude et alertes - ## Installation - 1. Cloner le dépôt et positionnez-vous dans le dossier du service : +- TENTATIVE_FRAUDE +- TRANSACTION_SUSPECTE +- ACTIVITE_INHABITUELLE - ```bash - cd notification_service - ``` +### 10. Systeme - 2. Installer les dépendances : - - ```bash - npm install - ``` - - 3. Compiler TypeScript : - - ```bash - npm run build - ``` - - 4. Lancer en développement (reload automatique) : - - ```bash - npm run dev - ``` - - ## Variables d'environnement - - Les variables attendues par le service (fichier `.env` recommandé) : - - SERVICE_PORT: port d'écoute HTTP (ex: 8000) - - SERVICE_VERSION: version déployée (optionnel) - - COMMIT_SHA: sha du commit déployé (optionnel) +- MAINTENANCE +- MISE_A_JOUR_SYSTEME +- ANNONCE - - PostgreSQL: - - DB_HOST - - DB_PORT (par défaut 5432) - - DB_USER - - DB_PASSWORD - - DB_NAME +## Notes d'exploitation - - RabbitMQ: - - RABBITMQ_URL (ex: amqp://user:pass@host:5672) - - RABBITMQ_EXCHANGE (nom de l'exchange partagé) - - RABBITMQ_QUEUE (nom de la queue principale pour ce service) - - - Twilio (si SMS) : - - TWILIO_ACCOUNT_SID - - TWILIO_AUTH_TOKEN - - TWILIO_PHONE_NUMBER - - - E-mail (Nodemailer) : - - MAIL_USER - - MAIL_PASS - - - Health / diagnostics (optionnel) : - - HEALTH_CHECK_TIMEOUT_MS (ms, défaut 1000) - - HEALTH_CACHE_TTL_MS (ms, défaut 5000) - - HEALTH_EXPOSE_ERRORS (true|false, défaut false) - - ## Commandes utiles - - `npm run dev` — démarre avec `ts-node-dev` (dev hot-reload) - - `npm run build` — compile TypeScript vers `dist/` - - `npm start` — exécute `node src/server.ts` (production si compilé) - - ## Endpoints et exemples - - Base URL: `http://{host}:{SERVICE_PORT}` - - Health - - `GET /health` — liveness minimal (retourne OK + uptime) - - `GET /health/ready` — readiness : vérifie PostgreSQL et RabbitMQ, retourne 200 ou 503. Réponse contient `components.db` et `components.rabbitmq`. - - Notifications - - `POST /api/notifications/envoyer` — envoie une notification. - - Corps possible (exemples) : - - Transfer (expéditeur + destinataire envoyés sur SMS + email si fournis) : - - ```json - { - "type": "transfer", - "sender": { "email": "a@ex.com", "phone": "+223xxxxxxxx" }, - "receiver": { "email": "b@ex.com", "phone": "+223yyyyyyyy" }, - "amount": 10000, - "content": "Votre transfert de 10000 F CFA a été effectué" - } - ``` - - Simple notification : - - ```json - { - "type": "alert_securite", - "user": { "email": "u@ex.com", "phone": "+223zzzzzzzz" }, - "content": "Un événement important a eu lieu" - } - ``` - - - Réponse : `201` + objet décrivant les enregistrements créés (sms / email) - - - `POST /api/notifications/rabbitmq` — endpoint de test qui publie un message sur RabbitMQ (routingKey/message dans body) - - OTP - - `POST /api/notifications/otp/generate` — génère un OTP - - Body example: - ```json - { - "utilisateurId": "user-123", - "canalNotification": "SMS", - "phone": "+223..." - } - ``` - - - `POST /api/notifications/otp/verify` — vérifie un OTP - - Body example: - ```json - { "utilisateurId": "user-123", "code": "1234" } - ``` - - ## Health checks (détails) - - `/health` est une probe de liveness simple, utile pour Kubernetes readiness/liveness probes basiques. - - `/health/ready` exécute des vérifications actives : - - exécute `SELECT 1` sur PostgreSQL (avec timeout configurable) - - vérifie que le channel RabbitMQ est initialisé - - met en cache le résultat pendant `HEALTH_CACHE_TTL_MS` pour limiter la charge - - renvoie `version` et `commit` si disponibles - - ## Docker / Compose - - Le repo contient un `Dockerfile` et un `docker-compose.yml` : - - Construction : - - ```bash - docker build -t ricash/notification-service:latest . - ``` - - Compose (exemple très simple) : - - ```yaml - version: "3.8" - services: - notification-service: - image: ricash/notification-service:latest - env_file: .env - ports: - - "8000:8000" - depends_on: - - db - - rabbitmq - - db: - image: postgres:15 - environment: - POSTGRES_USER: example - POSTGRES_PASSWORD: example - POSTGRES_DB: ricash - - rabbitmq: - image: rabbitmq:3-management - ports: - - "5672:5672" - - "15672:15672" - ``` - - ## Débogage et logs - - Les logs sont écrits sur stdout. - - Vérifier les erreurs de connexion à RabbitMQ et PostgreSQL au démarrage. - - En cas d'erreurs d'envoi SMS/Email, les exceptions sont loggées et le statut de la notification est mis à `ECHEC`. - - ## Sécurité et bonnes pratiques - - Ne pas exposer `HEALTH_EXPOSE_ERRORS=true` en production si les messages d'erreur contiennent des données sensibles. - - Utiliser des secrets manager pour les identifiants (DB, Twilio, MAIL_PASS). - - Désactiver `synchronize: true` (TypeORM) en production et utiliser des migrations contrôlées. - - ## Contribution - - Pour proposer des améliorations : - 1. Créer une branche feature - 2. Ajouter tests / valider localement - 3. Ouvrir une Pull Request vers `develop` - - ## Support - - Si tu veux, je peux : - - ajouter des exemples Postman - - créer un `docker-compose.dev.yml` complet pour démarrer la stack locale - - ajouter des tests unitaires pour `NotificationService` / `OtpService` - - *** - - Fait avec ❤️ — Notification-Service +- En cas d'erreur SMS/email, le statut est marque ECHEC. +- Pour la production, preferer des migrations TypeORM controlees plutot que synchronize. +- Eviter d'exposer les erreurs internes du health endpoint en production. diff --git a/dist/controllers/notificationController.js b/dist/controllers/notificationController.js index 275428a..a75ad9b 100644 --- a/dist/controllers/notificationController.js +++ b/dist/controllers/notificationController.js @@ -10,6 +10,10 @@ const ContactSchema = zod_1.z.object({ email: zod_1.z.string().email(), phone: zod_1.z.string().min(8), }); +const AlertContactSchema = zod_1.z.object({ + phone: zod_1.z.string().min(8), + email: zod_1.z.string().email().optional(), +}); const TransferNotificationSchema = zod_1.z.object({ type: zod_1.z.literal("transfer"), sender: ContactSchema, @@ -17,18 +21,26 @@ const TransferNotificationSchema = zod_1.z.object({ amount: zod_1.z.number().positive(), content: zod_1.z.string().min(1), }); +const AlertNotificationSchema = zod_1.z.object({ + type: zod_1.z.enum(["alert_securite", "ALERT_SECURITE"]), + user: AlertContactSchema, + content: zod_1.z.string().min(1), +}); const SimpleNotificationSchema = zod_1.z.object({ type: zod_1.z .string() .min(1) - .refine((value) => value !== "transfer", { - message: 'Utiliser le schéma "transfer" lorsque type = "transfer".', + .refine((value) => value !== "transfer" && + value !== "alert_securite" && + value !== "ALERT_SECURITE", { + message: 'Utiliser le schéma "transfer" ou "alert_securite" selon le type.', }), user: ContactSchema, content: zod_1.z.string().min(1), }); const NotificationBodySchema = zod_1.z.union([ TransferNotificationSchema, + AlertNotificationSchema, SimpleNotificationSchema, ]); const envoyerNotification = async (req, res) => { diff --git a/dist/controllers/optController.js b/dist/controllers/optController.js index ada808b..4c6f4ae 100644 --- a/dist/controllers/optController.js +++ b/dist/controllers/optController.js @@ -6,9 +6,7 @@ const Notification_1 = require("../entities/Notification"); const otpService_1 = require("../services/otpService"); const otpService = new otpService_1.OtpService(); const GenerateOtpSchema = zod_1.z.object({ - utilisateurId: zod_1.z.string().min(1), - canalNotification: zod_1.z.enum(["SMS", "EMAIL"]), - email: zod_1.z.string().email(), + utilisateurId: zod_1.z.string().min(1).optional(), phone: zod_1.z.string().min(8), }); const generateOtp = async (req, res) => { @@ -21,11 +19,9 @@ const generateOtp = async (req, res) => { errors: parsed.error.flatten(), }); } - const { utilisateurId, canalNotification, email, phone } = parsed.data; - const canalEnum = canalNotification === "SMS" - ? Notification_1.CanalNotification.SMS - : Notification_1.CanalNotification.EMAIL; - const result = await otpService.createOtp(utilisateurId, canalEnum, email, phone); + const { phone } = parsed.data; + const utilisateurId = parsed.data.utilisateurId ?? phone; + const result = await otpService.createOtp(utilisateurId, Notification_1.CanalNotification.SMS, phone); res.json(result); } catch (error) { diff --git a/dist/entities/Notification.js b/dist/entities/Notification.js index ce62a83..7bf88d2 100644 --- a/dist/entities/Notification.js +++ b/dist/entities/Notification.js @@ -13,7 +13,9 @@ exports.Notification = exports.StatutNotification = exports.CanalNotification = const typeorm_1 = require("typeorm"); var TypeNotification; (function (TypeNotification) { + // Existing types TypeNotification["CONFIRMATION_TRANSFERT"] = "CONFIRMATION_TRANSFERT"; + TypeNotification["CONFIRMATION_DEPOT"] = "CONFIRMATION_DEPOT"; TypeNotification["CONFIRMATION_RETRAIT"] = "CONFIRMATION_RETRAIT"; TypeNotification["RETRAIT_REUSSI"] = "RETRAIT_REUSSI"; TypeNotification["DEPOT_REUSSI"] = "DEPOT_REUSSI"; @@ -21,6 +23,58 @@ var TypeNotification; TypeNotification["VERIFICATION_KYC"] = "VERIFICATION_KYC"; TypeNotification["VERIFICATION_EMAIL"] = "VERIFICATION_EMAIL"; TypeNotification["VERIFICATION_TELEPHONE"] = "VERIFICATION_TELEPHONE"; + // 1. ADMIN MANAGEMENT + TypeNotification["ADMIN_CREE"] = "ADMIN_CREE"; + TypeNotification["ADMIN_MIS_A_JOUR"] = "ADMIN_MIS_A_JOUR"; + TypeNotification["ADMIN_SUPPRIME"] = "ADMIN_SUPPRIME"; + // 2. AGENT WORKFLOW + TypeNotification["AGENT_INSCRIPTION"] = "AGENT_INSCRIPTION"; + TypeNotification["AGENT_EN_ATTENTE_VALIDATION"] = "AGENT_EN_ATTENTE_VALIDATION"; + TypeNotification["AGENT_VALIDE"] = "AGENT_VALIDE"; + TypeNotification["AGENT_REJETE"] = "AGENT_REJETE"; + // 3. CLIENT + TypeNotification["CLIENT_INSCRIPTION"] = "CLIENT_INSCRIPTION"; + TypeNotification["CLIENT_COMPTE_ACTIF"] = "CLIENT_COMPTE_ACTIF"; + // 4. AUTHENTICATION AND SECURITY + TypeNotification["CONNEXION_REUSSIE"] = "CONNEXION_REUSSIE"; + TypeNotification["ECHEC_CONNEXION"] = "ECHEC_CONNEXION"; + TypeNotification["DECONNEXION"] = "DECONNEXION"; + TypeNotification["NOUVEL_APPAREIL"] = "NOUVEL_APPAREIL"; + TypeNotification["CHANGEMENT_MOT_DE_PASSE"] = "CHANGEMENT_MOT_DE_PASSE"; + TypeNotification["CHANGEMENT_EMAIL"] = "CHANGEMENT_EMAIL"; + TypeNotification["CHANGEMENT_TELEPHONE"] = "CHANGEMENT_TELEPHONE"; + TypeNotification["COMPTE_BLOQUE"] = "COMPTE_BLOQUE"; + TypeNotification["COMPTE_DEBLOQUE"] = "COMPTE_DEBLOQUE"; + // 5. TRANSACTIONS + TypeNotification["TRANSFERT_ENVOYE"] = "TRANSFERT_ENVOYE"; + TypeNotification["TRANSFERT_RECU"] = "TRANSFERT_RECU"; + TypeNotification["ECHEC_TRANSFERT"] = "ECHEC_TRANSFERT"; + TypeNotification["DEPOT_EN_COURS"] = "DEPOT_EN_COURS"; + TypeNotification["ECHEC_DEPOT"] = "ECHEC_DEPOT"; + TypeNotification["RETRAIT_EN_COURS"] = "RETRAIT_EN_COURS"; + TypeNotification["ECHEC_RETRAIT"] = "ECHEC_RETRAIT"; + // 6. OTP AND VERIFICATION + TypeNotification["OTP_ENVOYE"] = "OTP_ENVOYE"; + TypeNotification["OTP_VALIDE"] = "OTP_VALIDE"; + TypeNotification["OTP_EXPIRE"] = "OTP_EXPIRE"; + TypeNotification["OTP_INVALIDE"] = "OTP_INVALIDE"; + // 7. KYC + TypeNotification["KYC_EN_COURS"] = "KYC_EN_COURS"; + TypeNotification["KYC_VALIDE"] = "KYC_VALIDE"; + TypeNotification["KYC_REJETE"] = "KYC_REJETE"; + // 8. PAYMENT + TypeNotification["PAIEMENT_REUSSI"] = "PAIEMENT_REUSSI"; + TypeNotification["PAIEMENT_ECHOUE"] = "PAIEMENT_ECHOUE"; + TypeNotification["FACTURE_GENEREE"] = "FACTURE_GENEREE"; + TypeNotification["FACTURE_PAYEE"] = "FACTURE_PAYEE"; + // 9. FRAUD AND ALERTS + TypeNotification["TENTATIVE_FRAUDE"] = "TENTATIVE_FRAUDE"; + TypeNotification["TRANSACTION_SUSPECTE"] = "TRANSACTION_SUSPECTE"; + TypeNotification["ACTIVITE_INHABITUELLE"] = "ACTIVITE_INHABITUELLE"; + // 10. SYSTEM + TypeNotification["MAINTENANCE"] = "MAINTENANCE"; + TypeNotification["MISE_A_JOUR_SYSTEME"] = "MISE_A_JOUR_SYSTEME"; + TypeNotification["ANNONCE"] = "ANNONCE"; })(TypeNotification || (exports.TypeNotification = TypeNotification = {})); var CanalNotification; (function (CanalNotification) { diff --git a/dist/services/notificationService.js b/dist/services/notificationService.js index 8b3e658..ed1d78b 100644 --- a/dist/services/notificationService.js +++ b/dist/services/notificationService.js @@ -38,6 +38,10 @@ class NotificationService { // } // } mapStringToTypeNotification(type) { + const normalized = type.trim().toUpperCase(); + if (Object.values(Notification_1.TypeNotification).includes(normalized)) { + return normalized; + } switch (type) { case "transfer": return Notification_1.TypeNotification.CONFIRMATION_TRANSFERT; @@ -114,6 +118,58 @@ class NotificationService { email: notifEmail, }; } + async sendSmsPriorityToContact(contact, content, type, role, extraContext) { + const context = { ...(extraContext || {}), role }; + const notifSms = this.notifRepo.create({ + utilisateurId: contact.phone, + typeNotification: type, + canal: Notification_1.CanalNotification.SMS, + context, + message: content, + destinationPhone: contact.phone, + statut: Notification_1.StatutNotification.EN_COURS, + }); + await this.notifRepo.save(notifSms); + try { + await client.messages.create({ + body: content, + from: process.env.TWILIO_PHONE_NUMBER, + to: contact.phone, + }); + notifSms.statut = Notification_1.StatutNotification.ENVOYEE; + } + catch (error) { + notifSms.statut = Notification_1.StatutNotification.ECHEC; + console.error("Erreur d'envoi SMS :", error); + } + await this.notifRepo.save(notifSms); + let notifEmail; + if (contact.email) { + notifEmail = this.notifRepo.create({ + utilisateurId: contact.email, + typeNotification: type, + canal: Notification_1.CanalNotification.EMAIL, + context, + message: content, + destinationEmail: contact.email, + statut: Notification_1.StatutNotification.EN_COURS, + }); + await this.notifRepo.save(notifEmail); + try { + await (0, mailService_1.sendEmail)(contact.email, "Notification", content); + notifEmail.statut = Notification_1.StatutNotification.ENVOYEE; + } + catch (error) { + notifEmail.statut = Notification_1.StatutNotification.ECHEC; + console.error("Erreur d'envoi email :", error); + } + await this.notifRepo.save(notifEmail); + } + return { + sms: notifSms, + ...(notifEmail ? { email: notifEmail } : {}), + }; + } /** * Endpoint HTTP (Postman) : * - dépend UNIQUEMENT des coordonnées fournies dans le JSON @@ -131,6 +187,15 @@ class NotificationService { receiver: receiverResult, }; } + if (payload.type === "alert_securite" || + payload.type === "ALERT_SECURITE") { + const alertPayload = payload; + const type = this.mapStringToTypeNotification(alertPayload.type); + const userResult = await this.sendSmsPriorityToContact(alertPayload.user, alertPayload.content, type, "USER"); + return { + user: userResult, + }; + } const simplePayload = payload; const type = this.mapStringToTypeNotification(simplePayload.type); const userResult = await this.sendMultiChannelToContact(simplePayload.user, simplePayload.content, type, "USER"); @@ -158,6 +223,63 @@ class NotificationService { if (!destinationEmail && !destinationPhone) { throw new Error(`Aucun contact (email ou téléphone) disponible pour l'utilisateur ${data.utilisateurId}`); } + // Priorité SMS pour les alertes sécurité: SMS d'abord si numéro disponible, + // puis email en second canal si disponible. + if (data.typeNotification === Notification_1.TypeNotification.ALERT_SECURITE) { + const context = data.context; + let smsNotif; + let emailNotif; + if (destinationPhone) { + smsNotif = this.notifRepo.create({ + utilisateurId: data.utilisateurId, + typeNotification: data.typeNotification, + canal: Notification_1.CanalNotification.SMS, + context, + message, + destinationPhone, + statut: Notification_1.StatutNotification.EN_COURS, + }); + await this.notifRepo.save(smsNotif); + try { + await client.messages.create({ + body: message, + from: process.env.TWILIO_PHONE_NUMBER, + to: destinationPhone, + }); + smsNotif.statut = Notification_1.StatutNotification.ENVOYEE; + } + catch (error) { + smsNotif.statut = Notification_1.StatutNotification.ECHEC; + console.error("Erreur d'envoi SMS :", error); + } + await this.notifRepo.save(smsNotif); + } + if (destinationEmail) { + emailNotif = this.notifRepo.create({ + utilisateurId: data.utilisateurId, + typeNotification: data.typeNotification, + canal: Notification_1.CanalNotification.EMAIL, + context, + message, + destinationEmail, + statut: Notification_1.StatutNotification.EN_COURS, + }); + await this.notifRepo.save(emailNotif); + try { + await (0, mailService_1.sendEmail)(destinationEmail, "RICASH NOTIFICATION", message); + emailNotif.statut = Notification_1.StatutNotification.ENVOYEE; + } + catch (error) { + emailNotif.statut = Notification_1.StatutNotification.ECHEC; + console.error("Erreur d'envoi email :", error); + } + await this.notifRepo.save(emailNotif); + } + return { + ...(smsNotif ? { sms: smsNotif } : {}), + ...(emailNotif ? { email: emailNotif } : {}), + }; + } // 4. Validation spécifique au canal demandé if (data.canal === Notification_1.CanalNotification.EMAIL && !destinationEmail) { throw new Error(`Canal EMAIL demandé mais aucune adresse email valide pour l'utilisateur ${data.utilisateurId}`); diff --git a/dist/services/otpService.js b/dist/services/otpService.js index bcaa564..8c31d1c 100644 --- a/dist/services/otpService.js +++ b/dist/services/otpService.js @@ -13,31 +13,25 @@ class OtpService { generateCode() { return Math.floor(1000 + Math.random() * 9000).toString(); // 4chiffres } - async createOtp(utilisateurId, canalNotification, email, phone) { + async createOtp(utilisateurId, canalNotification, phone) { const code = this.generateCode(); const expiration = new Date(Date.now() + this.expirationDelay); - const destinationEmail = email; const destinationPhone = phone; const otp = this.otpRepo.create({ utilisateurId, // identifiant métier canal: canalNotification, code, expiration, - destinationEmail, destinationPhone, }); await this.otpRepo.save(otp); - // Détermination automatique du type de notification - const notifType = canalNotification === "EMAIL" - ? Notification_1.TypeNotification.VERIFICATION_EMAIL - : Notification_1.TypeNotification.VERIFICATION_TELEPHONE; + const notifType = Notification_1.TypeNotification.VERIFICATION_TELEPHONE; // message standard inter-services (aligné sur InterServices / NotificationEvent) const message = { utilisateurId, typeNotification: notifType, canal: canalNotification, context: { code }, - email: destinationEmail, phone: destinationPhone, metadata: { service: "notification-service:otp", diff --git a/src/controllers/notificationController.ts b/src/controllers/notificationController.ts index 7154609..3b1c795 100644 --- a/src/controllers/notificationController.ts +++ b/src/controllers/notificationController.ts @@ -10,6 +10,11 @@ const ContactSchema = z.object({ phone: z.string().min(8), }); +const AlertContactSchema = z.object({ + phone: z.string().min(8), + email: z.string().email().optional(), +}); + const TransferNotificationSchema = z.object({ type: z.literal("transfer"), sender: ContactSchema, @@ -18,19 +23,33 @@ const TransferNotificationSchema = z.object({ content: z.string().min(1), }); +const AlertNotificationSchema = z.object({ + type: z.enum(["alert_securite", "ALERT_SECURITE"]), + user: AlertContactSchema, + content: z.string().min(1), +}); + const SimpleNotificationSchema = z.object({ type: z .string() .min(1) - .refine((value) => value !== "transfer", { - message: 'Utiliser le schéma "transfer" lorsque type = "transfer".', - }), + .refine( + (value) => + value !== "transfer" && + value !== "alert_securite" && + value !== "ALERT_SECURITE", + { + message: + 'Utiliser le schéma "transfer" ou "alert_securite" selon le type.', + }, + ), user: ContactSchema, content: z.string().min(1), }); const NotificationBodySchema = z.union([ TransferNotificationSchema, + AlertNotificationSchema, SimpleNotificationSchema, ]); diff --git a/src/controllers/optController.ts b/src/controllers/optController.ts index b7dbf01..49ba3a6 100644 --- a/src/controllers/optController.ts +++ b/src/controllers/optController.ts @@ -6,9 +6,7 @@ import { OtpService } from "../services/otpService"; const otpService = new OtpService(); const GenerateOtpSchema = z.object({ - utilisateurId: z.string().min(1), - canalNotification: z.enum(["SMS", "EMAIL"]), - email: z.string().email(), + utilisateurId: z.string().min(1).optional(), phone: z.string().min(8), }); @@ -24,17 +22,12 @@ export const generateOtp = async (req: Request, res: Response) => { }); } - const { utilisateurId, canalNotification, email, phone } = parsed.data; - - const canalEnum = - canalNotification === "SMS" - ? CanalNotification.SMS - : CanalNotification.EMAIL; + const { phone } = parsed.data; + const utilisateurId = parsed.data.utilisateurId ?? phone; const result = await otpService.createOtp( utilisateurId, - canalEnum, - email, + CanalNotification.SMS, phone, ); res.json(result); diff --git a/src/entities/Notification.ts b/src/entities/Notification.ts index 12c14fc..ce463f5 100644 --- a/src/entities/Notification.ts +++ b/src/entities/Notification.ts @@ -6,7 +6,9 @@ import { } from "typeorm"; export enum TypeNotification { + // Existing types CONFIRMATION_TRANSFERT = "CONFIRMATION_TRANSFERT", + CONFIRMATION_DEPOT = "CONFIRMATION_DEPOT", CONFIRMATION_RETRAIT = "CONFIRMATION_RETRAIT", RETRAIT_REUSSI = "RETRAIT_REUSSI", DEPOT_REUSSI = "DEPOT_REUSSI", @@ -14,6 +16,68 @@ export enum TypeNotification { VERIFICATION_KYC = "VERIFICATION_KYC", VERIFICATION_EMAIL = "VERIFICATION_EMAIL", VERIFICATION_TELEPHONE = "VERIFICATION_TELEPHONE", + + // 1. ADMIN MANAGEMENT + ADMIN_CREE = "ADMIN_CREE", + ADMIN_MIS_A_JOUR = "ADMIN_MIS_A_JOUR", + ADMIN_SUPPRIME = "ADMIN_SUPPRIME", + + // 2. AGENT WORKFLOW + AGENT_INSCRIPTION = "AGENT_INSCRIPTION", + AGENT_EN_ATTENTE_VALIDATION = "AGENT_EN_ATTENTE_VALIDATION", + AGENT_VALIDE = "AGENT_VALIDE", + AGENT_REJETE = "AGENT_REJETE", + + // 3. CLIENT + CLIENT_INSCRIPTION = "CLIENT_INSCRIPTION", + CLIENT_COMPTE_ACTIF = "CLIENT_COMPTE_ACTIF", + + // 4. AUTHENTICATION AND SECURITY + CONNEXION_REUSSIE = "CONNEXION_REUSSIE", + ECHEC_CONNEXION = "ECHEC_CONNEXION", + DECONNEXION = "DECONNEXION", + NOUVEL_APPAREIL = "NOUVEL_APPAREIL", + CHANGEMENT_MOT_DE_PASSE = "CHANGEMENT_MOT_DE_PASSE", + CHANGEMENT_EMAIL = "CHANGEMENT_EMAIL", + CHANGEMENT_TELEPHONE = "CHANGEMENT_TELEPHONE", + COMPTE_BLOQUE = "COMPTE_BLOQUE", + COMPTE_DEBLOQUE = "COMPTE_DEBLOQUE", + + // 5. TRANSACTIONS + TRANSFERT_ENVOYE = "TRANSFERT_ENVOYE", + TRANSFERT_RECU = "TRANSFERT_RECU", + ECHEC_TRANSFERT = "ECHEC_TRANSFERT", + DEPOT_EN_COURS = "DEPOT_EN_COURS", + ECHEC_DEPOT = "ECHEC_DEPOT", + RETRAIT_EN_COURS = "RETRAIT_EN_COURS", + ECHEC_RETRAIT = "ECHEC_RETRAIT", + + // 6. OTP AND VERIFICATION + OTP_ENVOYE = "OTP_ENVOYE", + OTP_VALIDE = "OTP_VALIDE", + OTP_EXPIRE = "OTP_EXPIRE", + OTP_INVALIDE = "OTP_INVALIDE", + + // 7. KYC + KYC_EN_COURS = "KYC_EN_COURS", + KYC_VALIDE = "KYC_VALIDE", + KYC_REJETE = "KYC_REJETE", + + // 8. PAYMENT + PAIEMENT_REUSSI = "PAIEMENT_REUSSI", + PAIEMENT_ECHOUE = "PAIEMENT_ECHOUE", + FACTURE_GENEREE = "FACTURE_GENEREE", + FACTURE_PAYEE = "FACTURE_PAYEE", + + // 9. FRAUD AND ALERTS + TENTATIVE_FRAUDE = "TENTATIVE_FRAUDE", + TRANSACTION_SUSPECTE = "TRANSACTION_SUSPECTE", + ACTIVITE_INHABITUELLE = "ACTIVITE_INHABITUELLE", + + // 10. SYSTEM + MAINTENANCE = "MAINTENANCE", + MISE_A_JOUR_SYSTEME = "MISE_A_JOUR_SYSTEME", + ANNONCE = "ANNONCE", } export enum CanalNotification { diff --git a/src/messaging/contracts/interServices.ts b/src/messaging/contracts/interServices.ts index dc6ee1f..b552151 100644 --- a/src/messaging/contracts/interServices.ts +++ b/src/messaging/contracts/interServices.ts @@ -1,14 +1,8 @@ +import { TypeNotification } from "../../entities/Notification"; + export interface InterServices { utilisateurId: string; - typeNotification: - | "CONFIRMATION_TRANSFERT" - | "RETRAIT_REUSSI" - | "DEPOT_REUSSI" - | "ALERT_SECURITE" - | "CONFIRMATION_DEPOT" - | "VERIFICATION_EMAIL" - | "VERIFICATION_TELEPHONE" - | "VERIFICATION_KYC"; + typeNotification: TypeNotification; canal: "SMS" | "EMAIL" | "PUSH"; /** diff --git a/src/services/notificationService.ts b/src/services/notificationService.ts index 3fb11b0..c290ed0 100644 --- a/src/services/notificationService.ts +++ b/src/services/notificationService.ts @@ -23,6 +23,11 @@ export interface ContactInfoDTO { phone: string; } +export interface AlertContactInfoDTO { + phone: string; + email?: string; +} + export interface TransferNotificationDTO { type: "transfer"; sender: ContactInfoDTO; @@ -37,8 +42,15 @@ export interface SimpleNotificationDTO { content: string; } +export interface AlertNotificationDTO { + type: "alert_securite" | "ALERT_SECURITE"; + user: AlertContactInfoDTO; + content: string; +} + export type HttpNotificationDTO = | TransferNotificationDTO + | AlertNotificationDTO | SimpleNotificationDTO; export class NotificationService { @@ -69,6 +81,12 @@ export class NotificationService { // } private mapStringToTypeNotification(type: string): TypeNotification { + const normalized = type.trim().toUpperCase(); + + if ((Object.values(TypeNotification) as string[]).includes(normalized)) { + return normalized as TypeNotification; + } + switch (type) { case "transfer": return TypeNotification.CONFIRMATION_TRANSFERT; @@ -160,6 +178,72 @@ export class NotificationService { }; } + private async sendSmsPriorityToContact( + contact: AlertContactInfoDTO, + content: string, + type: TypeNotification, + role: string, + extraContext?: Record, + ) { + const context = { ...(extraContext || {}), role }; + + const notifSms = this.notifRepo.create({ + utilisateurId: contact.phone, + typeNotification: type, + canal: CanalNotification.SMS, + context, + message: content, + destinationPhone: contact.phone, + statut: StatutNotification.EN_COURS, + }); + + await this.notifRepo.save(notifSms); + + try { + await client.messages.create({ + body: content, + from: process.env.TWILIO_PHONE_NUMBER, + to: contact.phone, + }); + notifSms.statut = StatutNotification.ENVOYEE; + } catch (error) { + notifSms.statut = StatutNotification.ECHEC; + console.error("Erreur d'envoi SMS :", error); + } + + await this.notifRepo.save(notifSms); + + let notifEmail: Notification | undefined; + if (contact.email) { + notifEmail = this.notifRepo.create({ + utilisateurId: contact.email, + typeNotification: type, + canal: CanalNotification.EMAIL, + context, + message: content, + destinationEmail: contact.email, + statut: StatutNotification.EN_COURS, + }); + + await this.notifRepo.save(notifEmail); + + try { + await sendEmail(contact.email, "Notification", content); + notifEmail.statut = StatutNotification.ENVOYEE; + } catch (error) { + notifEmail.statut = StatutNotification.ECHEC; + console.error("Erreur d'envoi email :", error); + } + + await this.notifRepo.save(notifEmail); + } + + return { + sms: notifSms, + ...(notifEmail ? { email: notifEmail } : {}), + }; + } + /** * Endpoint HTTP (Postman) : * - dépend UNIQUEMENT des coordonnées fournies dans le JSON @@ -192,6 +276,25 @@ export class NotificationService { }; } + if ( + payload.type === "alert_securite" || + payload.type === "ALERT_SECURITE" + ) { + const alertPayload = payload as AlertNotificationDTO; + const type = this.mapStringToTypeNotification(alertPayload.type); + + const userResult = await this.sendSmsPriorityToContact( + alertPayload.user, + alertPayload.content, + type, + "USER", + ); + + return { + user: userResult, + }; + } + const simplePayload = payload as SimpleNotificationDTO; const type = this.mapStringToTypeNotification(simplePayload.type); @@ -242,6 +345,69 @@ export class NotificationService { ); } + // Priorité SMS pour les alertes sécurité: SMS d'abord si numéro disponible, + // puis email en second canal si disponible. + if (data.typeNotification === TypeNotification.ALERT_SECURITE) { + const context = data.context; + let smsNotif: Notification | undefined; + let emailNotif: Notification | undefined; + + if (destinationPhone) { + smsNotif = this.notifRepo.create({ + utilisateurId: data.utilisateurId, + typeNotification: data.typeNotification, + canal: CanalNotification.SMS, + context, + message, + destinationPhone, + statut: StatutNotification.EN_COURS, + }); + await this.notifRepo.save(smsNotif); + + try { + await client.messages.create({ + body: message, + from: process.env.TWILIO_PHONE_NUMBER, + to: destinationPhone, + }); + smsNotif.statut = StatutNotification.ENVOYEE; + } catch (error) { + smsNotif.statut = StatutNotification.ECHEC; + console.error("Erreur d'envoi SMS :", error); + } + + await this.notifRepo.save(smsNotif); + } + + if (destinationEmail) { + emailNotif = this.notifRepo.create({ + utilisateurId: data.utilisateurId, + typeNotification: data.typeNotification, + canal: CanalNotification.EMAIL, + context, + message, + destinationEmail, + statut: StatutNotification.EN_COURS, + }); + await this.notifRepo.save(emailNotif); + + try { + await sendEmail(destinationEmail, "RICASH NOTIFICATION", message); + emailNotif.statut = StatutNotification.ENVOYEE; + } catch (error) { + emailNotif.statut = StatutNotification.ECHEC; + console.error("Erreur d'envoi email :", error); + } + + await this.notifRepo.save(emailNotif); + } + + return { + ...(smsNotif ? { sms: smsNotif } : {}), + ...(emailNotif ? { email: emailNotif } : {}), + }; + } + // 4. Validation spécifique au canal demandé if (data.canal === CanalNotification.EMAIL && !destinationEmail) { throw new Error( diff --git a/src/services/otpService.ts b/src/services/otpService.ts index c1fb0f3..eb17723 100644 --- a/src/services/otpService.ts +++ b/src/services/otpService.ts @@ -15,30 +15,23 @@ export class OtpService { async createOtp( utilisateurId: string, - canalNotification: CanalNotification.EMAIL | CanalNotification.SMS, - email: string, + canalNotification: CanalNotification.SMS, phone: string, ) { const code = this.generateCode(); const expiration = new Date(Date.now() + this.expirationDelay); - const destinationEmail: string = email; - const destinationPhone: string = phone; + const destinationPhone = phone; const otp = this.otpRepo.create({ utilisateurId, // identifiant métier canal: canalNotification, code, expiration, - destinationEmail, destinationPhone, }); await this.otpRepo.save(otp); - // Détermination automatique du type de notification - const notifType = - canalNotification === "EMAIL" - ? TypeNotification.VERIFICATION_EMAIL - : TypeNotification.VERIFICATION_TELEPHONE; + const notifType = TypeNotification.VERIFICATION_TELEPHONE; // message standard inter-services (aligné sur InterServices / NotificationEvent) const message: InterServices = { @@ -46,7 +39,6 @@ export class OtpService { typeNotification: notifType, canal: canalNotification, context: { code }, - email: destinationEmail, phone: destinationPhone, metadata: { service: "notification-service:otp",