Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
595 changes: 345 additions & 250 deletions README.md

Large diffs are not rendered by default.

16 changes: 14 additions & 2 deletions dist/controllers/notificationController.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,25 +10,37 @@ 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,
receiver: ContactSchema,
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) => {
Expand Down
12 changes: 4 additions & 8 deletions dist/controllers/optController.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) => {
Expand All @@ -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) {
Expand Down
54 changes: 54 additions & 0 deletions dist/entities/Notification.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,68 @@ 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";
TypeNotification["ALERT_SECURITE"] = "ALERT_SECURITE";
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) {
Expand Down
122 changes: 122 additions & 0 deletions dist/services/notificationService.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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
Expand All @@ -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");
Expand Down Expand Up @@ -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}`);
Expand Down
10 changes: 2 additions & 8 deletions dist/services/otpService.js
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
25 changes: 22 additions & 3 deletions src/controllers/notificationController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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,
]);

Expand Down
15 changes: 4 additions & 11 deletions src/controllers/optController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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),
});

Expand All @@ -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);
Expand Down
Loading
Loading