# 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<string, unknown>;
}

export interface SendChannelMessageResult {
  messageId: string;
}

export interface NotificationChannelPort {
  readonly channel: NotificationChannel;  // 'EMAIL' | 'SMS' | 'PUSH' | 'ZALO_OA'
  readonly isAvailable: boolean;
  send(dto: SendChannelMessageDto): Promise<SendChannelMessageResult>;
}

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

