# Notifications Module - Complete Exploration ## 📊 Summary - **Total Files**: 67 TypeScript files - **Architecture**: DDD + CQRS - **Channels**: EMAIL, SMS, PUSH (FCM), ZALO_OA - **Event Listeners**: 18 domain event listeners - **Current SMS Provider**: Stringee (fully implemented) --- ## 🎯 Main Interface: NotificationChannelPort **Location**: `domain/ports/notification-channel.port.ts` ```typescript export interface SendChannelMessageDto { recipient: string; // Email/phone/token subject: string; // For EMAIL body: string; // HTML content templateKey: string; // e.g., 'user.registered' metadata?: Record; } export interface SendChannelMessageResult { messageId: string; } export interface NotificationChannelPort { readonly channel: NotificationChannel; // 'EMAIL' | 'SMS' | 'PUSH' | 'ZALO_OA' readonly isAvailable: boolean; send(dto: SendChannelMessageDto): Promise; } export const SMS_NOTIFICATION_CHANNEL = Symbol('SMS_NOTIFICATION_CHANNEL'); ``` --- ## 📡 SMS Rate Limiting **Location**: `infrastructure/services/sms-rate-limiter.service.ts` - **Technology**: Redis + Lua script (atomic, sliding window) - **Buckets**: - `otp`: 5/min, 10/hour (strict) - `transactional`: 20/min, 100/hour (lenient) - **Graceful degradation**: Redis error → allow request --- ## 🔧 Stringee SMS Service (Reference Implementation) **Location**: `infrastructure/services/stringee-sms.service.ts` ### Constructor Dependencies - `LoggerService` - `SmsRateLimiterService` ### Environment Variables - `STRINGEE_API_KEY` (required) - `STRINGEE_BRANDNAME` (optional, default: "GoodGo") ### Key Methods 1. `onModuleInit()` - Read env, validate, initialize 2. `get isAvailable()` - Check if initialized 3. `async send(dto)` - NotificationChannelPort implementation 4. `async sendOTP(dto)` - Specific OTP handling 5. `async dispatch(dto)` - Main workflow 6. `async enforceRateLimit()` - Check per-minute AND hourly 7. `async sendWithRetry()` - 3 attempts with exponential backoff 8. `normalizePhone()` - +84xxx format conversion 9. `stripHtml()` - Remove HTML tags ### Workflow 1. Rate limit check (per-minute + hourly) 2. HTML stripping 3. Phone normalization (+84, 84, 0 formats) 4. Retry logic (1s → 2s → 4s) 5. API call to https://api.stringee.com/v1/sms 6. Return messageId --- ## 🏗️ DI Configuration **Location**: `notifications.module.ts` ```typescript @Module({ providers: [ // Repositories { provide: NOTIFICATION_REPOSITORY, useClass: PrismaNotificationRepository }, { provide: NOTIFICATION_PREFERENCE_REPOSITORY, useClass: PrismaNotificationPreferenceRepository }, // Services EmailService, FcmService, SmsRateLimiterService, StringeeSmsService, { provide: SMS_NOTIFICATION_CHANNEL, useExisting: StringeeSmsService }, ZaloOaService, TemplateService, // Others NotificationsGateway, SendNotificationHandler, ...EventListeners, ], exports: [ EmailService, FcmService, SmsRateLimiterService, StringeeSmsService, SMS_NOTIFICATION_CHANNEL, ZaloOaService, TemplateService, NotificationsGateway, ], }) export class NotificationsModule {} ``` --- ## 🔄 Message Flow ``` Domain Event (e.g., user.registered) ↓ @OnEvent('user.registered') Listener ↓ CommandBus.execute(SendNotificationCommand) ↓ SendNotificationHandler ├─ Check user preference (enabled?) ├─ Render template ├─ Persist notification log ├─ Dispatch to channel: │ ├─ EMAIL → EmailService.send() │ ├─ SMS → StringeeSmsService.send() [with rate limit + retry] │ ├─ PUSH → FcmService.send() │ └─ ZALO → ZaloOaService.send() ├─ Update status (SENT/FAILED) └─ Publish NotificationSentEvent ``` --- ## 💡 Key Patterns ### Pattern 1: OnModuleInit + Lazy Initialization - Read env vars in `onModuleInit()` - Set `initialized = true` only if valid - Check `isAvailable` getter before operations ### Pattern 2: DI Token Binding ```typescript export const MY_TOKEN = Symbol('MY_TOKEN'); providers: [ MyService, { provide: MY_TOKEN, useExisting: MyService } ] // Inject @Inject(MY_TOKEN) myService: SomeInterface ``` ### Pattern 3: Rate Limiting - Always call `rateLimiter.check(phone, bucket)` first - Support OTP vs transactional buckets - Check BOTH per-minute and hourly limits - Throw `DomainException` on violation ### Pattern 4: Error Handling ```typescript throw new DomainException( ErrorCode.TOO_MANY_REQUESTS, `SMS limit exceeded. Retry after ${retryAfterSeconds}s.`, HttpStatus.TOO_MANY_REQUESTS, { bucket, retryAfterSeconds } ); ``` ### Pattern 5: Retry Logic - MAX_RETRIES = 3 - Exponential backoff: 1s → 2s → 4s - Log each attempt, fail silently on final retry --- ## 📁 Directory Structure ``` notifications/ ├── domain/ │ ├── ports/ │ │ └── notification-channel.port.ts ⭐ Main interface │ ├── repositories/ ← DI Tokens │ ├── value-objects/ │ │ └── notification-channel.vo.ts ← Channel enum │ ├── entities/ │ └── events/ │ └── notification-sent.event.ts ├── infrastructure/ │ ├── services/ │ │ ├── stringee-sms.service.ts ⭐ SMS provider example │ │ ├── sms-rate-limiter.service.ts ← Rate limiting │ │ ├── email.service.ts │ │ ├── fcm.service.ts │ │ └── zalo-oa.service.ts │ └── repositories/ │ ├── prisma-notification.repository.ts │ └── prisma-notification-preference.repository.ts ├── application/ │ ├── commands/ │ │ └── send-notification/ │ │ ├── send-notification.command.ts │ │ └── send-notification.handler.ts ⭐ Command dispatcher │ └── listeners/ ← 18 event listeners ├── presentation/ │ ├── controllers/ │ └── gateways/ │ └── notifications.gateway.ts ← WebSocket ├── notifications.module.ts ⭐ DI setup └── index.ts ``` --- ## 🛠️ Implementation Checklist (for new SMS provider) - [ ] Create `infrastructure/services/{provider}-sms.service.ts` - [ ] Implement `NotificationChannelPort` interface - [ ] Implement `OnModuleInit` for env var reading - [ ] Inject `SmsRateLimiterService` - [ ] Implement rate limit enforcement - [ ] Implement phone normalization - [ ] Strip HTML from body - [ ] Implement retry logic (3 attempts, exponential backoff) - [ ] Register in `notifications.module.ts` - [ ] Integrate with `SendNotificationHandler` - [ ] Write unit tests --- ## 📊 SMS Rate Limits | Bucket | Per-Minute | Per-Hour | Use Case | |--------|-----------|----------|----------| | `otp` | 5 | 10 | OTP codes, verification | | `transactional` | 20 | 100 | Regular notifications | **OTP Templates** (use strict limits): - user.phone_change_otp - auth.login_otp - auth.kyc_otp - auth.phone_verify_otp --- ## 🔗 Quick Reference | File | Purpose | |------|---------| | `domain/ports/notification-channel.port.ts` | Interface + DI symbols | | `infrastructure/services/stringee-sms.service.ts` | SMS provider example | | `infrastructure/services/sms-rate-limiter.service.ts` | Rate limiting | | `application/commands/send-notification/send-notification.handler.ts` | Command handler | | `notifications.module.ts` | DI configuration | | `application/listeners/user-registered.listener.ts` | Event listener example | --- ## ✅ Everything You Need - [x] Full directory structure - [x] Main interface (NotificationChannelPort) - [x] Existing SMS implementation (Stringee - complete reference) - [x] Rate limiting system (Redis-based) - [x] DI configuration patterns - [x] Message flow understanding - [x] Handler integration patterns - [x] Error handling patterns - [x] Testing patterns