Aller au contenu principal

Module Notifications

Module de notifications push et email pour la MyTelevision API.

Statut

Ce module est en cours de developpement.


Vue d'ensemble

Le module Notifications gere :

  • Notifications push (FCM, APNs)
  • Notifications email (AWS SES, SendGrid)
  • Notifications in-app
  • Preferences de notification par profil
  • Notifications planifiees (scheduled)
  • Heures de silence (quiet hours)

Architecture


Types de Notifications

TypeCanalCas d'utilisation
NEW_CONTENTPush, EmailNouveau film/serie disponible
EPISODE_RELEASEDPushNouvel episode d'une serie suivie
SUBSCRIPTION_EXPIRINGPush, EmailAbonnement expirant bientot
PAYMENT_FAILEDPush, EmailEchec du traitement du paiement
ACCOUNT_SECURITYPush, EmailConnexion depuis un nouvel appareil
PROMOTIONALPush, EmailOffres speciales
SYSTEMIn-appAnnonces de maintenance

Modeles de Donnees

model Notification {
id String @id @default(uuid())
accountId String
profileId String?
type NotificationType
title String
body String
data Json?
imageUrl String?
actionUrl String?
status NotificationStatus @default(PENDING)
readAt DateTime?
sentAt DateTime?
failedAt DateTime?
failureReason String?

createdAt DateTime @default(now())
scheduledAt DateTime?

@@index([accountId, status])
@@index([profileId, status])
}

model NotificationPreference {
id String @id @default(uuid())
accountId String
profileId String?

// Preferences push
pushEnabled Boolean @default(true)
newContent Boolean @default(true)
episodes Boolean @default(true)
promotions Boolean @default(false)

// Preferences email
emailEnabled Boolean @default(true)
weeklyDigest Boolean @default(true)
marketing Boolean @default(false)

// Heures de silence
quietHoursEnabled Boolean @default(false)
quietHoursStart String? // "22:00"
quietHoursEnd String? // "08:00"

createdAt DateTime @default(now())
updatedAt DateTime @updatedAt

@@unique([accountId, profileId])
}

model DeviceToken {
id String @id @default(uuid())
accountId String
profileId String?
deviceId String
token String @unique
platform Platform // IOS, ANDROID, WEB
isActive Boolean @default(true)

createdAt DateTime @default(now())
updatedAt DateTime @updatedAt

@@index([accountId])
}

enum NotificationType {
NEW_CONTENT
EPISODE_RELEASED
SUBSCRIPTION_EXPIRING
PAYMENT_FAILED
ACCOUNT_SECURITY
PROMOTIONAL
SYSTEM
}

enum NotificationStatus {
PENDING
QUEUED
SENT
DELIVERED
READ
FAILED
}

enum Platform {
IOS
ANDROID
WEB
}

Composants Principaux

NotificationService

@Injectable()
export class NotificationService {
async send(dto: SendNotificationDto): Promise<Notification>;
async sendBulk(dto: SendBulkNotificationDto): Promise<BulkResult>;
async schedule(dto: ScheduleNotificationDto): Promise<Notification>;
async cancel(notificationId: string): Promise<void>;
async markAsRead(notificationId: string): Promise<Notification>;
async getForProfile(
profileId: string,
pagination: PaginationDto,
): Promise<PaginatedResponse<Notification>>;
async getUnreadCount(profileId: string): Promise<number>;
}

PushService

@Injectable()
export class PushService {
async sendPush(dto: PushNotificationDto): Promise<PushResult>;
async sendToDevice(
token: string,
notification: PushPayload,
): Promise<boolean>;
async sendToTopic(topic: string, notification: PushPayload): Promise<number>;
async registerDevice(dto: RegisterDeviceDto): Promise<DeviceToken>;
async unregisterDevice(deviceId: string): Promise<void>;
}

EmailService

@Injectable()
export class EmailService {
async send(dto: SendEmailDto): Promise<EmailResult>;
async sendTemplate(dto: SendTemplateEmailDto): Promise<EmailResult>;
async sendBulk(emails: SendEmailDto[]): Promise<BulkEmailResult>;
}

Flux de Notification


Endpoints API

MethodeEndpointDescriptionAuth
GET/notificationsLister les notificationsOui
GET/notifications/unread-countObtenir le nombre non luesOui
PATCH/notifications/:id/readMarquer comme lueOui
POST/notifications/read-allMarquer toutes comme luesOui
DELETE/notifications/:idSupprimer une notificationOui
GET/notifications/preferencesObtenir les preferencesOui
PATCH/notifications/preferencesMettre a jour les preferencesOui
POST/devices/registerEnregistrer un token d'appareilOui
DELETE/devices/:deviceIdDesenregistrer un appareilOui

Structure du Payload Push

Payload FCM

{
"notification": {
"title": "Nouvel Episode Disponible",
"body": "Breaking Bad S5E16 est maintenant en streaming",
"image": "https://cdn.mytv.app/images/bb.jpg"
},
"data": {
"type": "EPISODE_RELEASED",
"contentType": "EPISODE",
"contentId": "episode-uuid",
"seriesId": "series-uuid"
},
"android": {
"priority": "high",
"notification": {
"channel_id": "new_content"
}
},
"apns": {
"payload": {
"aps": {
"sound": "default",
"badge": 1
}
}
}
}

Templates Email

TemplateDeclencheurVariables
welcomeInscriptionfirstName, email
password_resetMot de passe oublieresetLink, expiresIn
subscription_confirmationNouvel abonnementplanName, amount, nextBillingDate
payment_failedEchec de paiementamount, retryDate
weekly_digestCron (hebdomadaire)newContent[], recommendations[]

Exemple de Template (Handlebars)

<!-- templates/welcome.hbs -->
<h1>Bienvenue sur MyTV, {{firstName}} !</h1>
<p>Votre compte a ete cree avec succes.</p>
<p>Commencez a explorer notre catalogue :</p>
<a href='{{ctaUrl}}' class='button'>Parcourir le Contenu</a>

Jobs Planifies

// Traitement de la file de notifications
@Cron('*/10 * * * * *') // Toutes les 10 secondes
async processQueue() {
const jobs = await this.queue.getJobs(10);
for (const job of jobs) {
await this.processNotification(job);
}
}

// Envoi du digest hebdomadaire
@Cron('0 9 * * 0') // Chaque dimanche a 9h
async sendWeeklyDigest() {
const subscribers = await this.getDigestSubscribers();
for (const subscriber of subscribers) {
await this.emailService.sendTemplate({
to: subscriber.email,
template: 'weekly_digest',
data: await this.buildDigestData(subscriber),
});
}
}

// Nettoyage des anciennes notifications
@Cron('0 2 * * *') // Chaque jour a 2h du matin
async cleanOldNotifications() {
await this.notificationService.deleteOlderThan(30); // 30 jours
}

Variables d'Environnement

# Firebase Cloud Messaging
FCM_PROJECT_ID=your-project-id
FCM_PRIVATE_KEY="-----BEGIN PRIVATE KEY-----\n...\n-----END PRIVATE KEY-----\n"
FCM_CLIENT_EMAIL=[email protected]

# Email (AWS SES)
AWS_SES_REGION=eu-west-1
AWS_SES_ACCESS_KEY_ID=xxx
AWS_SES_SECRET_ACCESS_KEY=xxx
EMAIL_FROM=[email protected]
EMAIL_FROM_NAME=MyTV

# Email (SendGrid - alternative)
SENDGRID_API_KEY=SG.xxx

Metriques Prometheus

mytelevision_notifications_sent_total{type, channel, status}
mytelevision_notifications_delivered_total{type, channel}
mytelevision_notifications_failed_total{type, channel, reason}
mytelevision_push_delivery_rate
mytelevision_email_open_rate
mytelevision_email_click_rate

Tests

describe('NotificationService', () => {
it('should send push notification', async () => {
const notification = await notificationService.send({
accountId: 'account-id',
type: NotificationType.NEW_CONTENT,
title: 'New Movie',
body: 'Check out our latest release!',
});

expect(notification.status).toBe(NotificationStatus.QUEUED);
});

it('should respect quiet hours', async () => {
// Definir les heures de silence
await setQuietHours('22:00', '08:00');

// Tentative d'envoi a 23:00
const notification = await notificationService.send({...});

// Devrait etre planifie pour 08:00
expect(notification.scheduledAt.getHours()).toBe(8);
});
});

Documentation associee