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",