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
| Type | Canal | Cas d'utilisation |
|---|---|---|
| NEW_CONTENT | Push, Email | Nouveau film/serie disponible |
| EPISODE_RELEASED | Push | Nouvel episode d'une serie suivie |
| SUBSCRIPTION_EXPIRING | Push, Email | Abonnement expirant bientot |
| PAYMENT_FAILED | Push, Email | Echec du traitement du paiement |
| ACCOUNT_SECURITY | Push, Email | Connexion depuis un nouvel appareil |
| PROMOTIONAL | Push, Email | Offres speciales |
| SYSTEM | In-app | Annonces 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
| Methode | Endpoint | Description | Auth |
|---|---|---|---|
| GET | /notifications | Lister les notifications | Oui |
| GET | /notifications/unread-count | Obtenir le nombre non lues | Oui |
| PATCH | /notifications/:id/read | Marquer comme lue | Oui |
| POST | /notifications/read-all | Marquer toutes comme lues | Oui |
| DELETE | /notifications/:id | Supprimer une notification | Oui |
| GET | /notifications/preferences | Obtenir les preferences | Oui |
| PATCH | /notifications/preferences | Mettre a jour les preferences | Oui |
| POST | /devices/register | Enregistrer un token d'appareil | Oui |
| DELETE | /devices/:deviceId | Desenregistrer un appareil | Oui |
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
| Template | Declencheur | Variables |
|---|---|---|
| welcome | Inscription | firstName, email |
| password_reset | Mot de passe oublie | resetLink, expiresIn |
| subscription_confirmation | Nouvel abonnement | planName, amount, nextBillingDate |
| payment_failed | Echec de paiement | amount, retryDate |
| weekly_digest | Cron (hebdomadaire) | newContent[], recommendations[] |
Exemple de Template (Handlebars)
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);
});
});