@nathapp/nestjs-notification 2.1.4 → 3.0.0-rc.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (123) hide show
  1. package/CLAUDE.md +21 -0
  2. package/UPGRADE-PLAN-PHASE7-8.md +307 -0
  3. package/UPGRADE-PLAN-PHASE9.md +361 -0
  4. package/UPGRADE-PLAN.md +425 -0
  5. package/dist/email/email.interface.d.ts +1 -1
  6. package/dist/email/email.provider.d.ts +2 -2
  7. package/dist/email/email.provider.js +7 -5
  8. package/dist/email/email.provider.js.map +1 -1
  9. package/dist/email/index.d.ts +1 -3
  10. package/dist/email/index.js +1 -6
  11. package/dist/email/index.js.map +1 -1
  12. package/dist/index.d.ts +4 -3
  13. package/dist/index.js +22 -3
  14. package/dist/index.js.map +1 -1
  15. package/dist/interface/index.d.ts +3 -3
  16. package/dist/interface/message.interface.d.ts +11 -4
  17. package/dist/interface/payload.interface.d.ts +71 -0
  18. package/dist/{push-notification/push-notification-module.interface.js → interface/payload.interface.js} +1 -2
  19. package/dist/interface/payload.interface.js.map +1 -0
  20. package/dist/interface/result.interface.d.ts +2 -1
  21. package/dist/notification/errors.d.ts +3 -0
  22. package/dist/notification/errors.js +11 -0
  23. package/dist/notification/errors.js.map +1 -0
  24. package/dist/notification/index.d.ts +6 -2
  25. package/dist/notification/index.js +7 -1
  26. package/dist/notification/index.js.map +1 -1
  27. package/dist/notification/notification-provider.factory.d.ts +5 -0
  28. package/dist/notification/notification-provider.factory.js +33 -0
  29. package/dist/notification/notification-provider.factory.js.map +1 -0
  30. package/dist/notification/notification.interceptor.d.ts +18 -0
  31. package/dist/notification/notification.interceptor.js +5 -0
  32. package/dist/notification/notification.interceptor.js.map +1 -0
  33. package/dist/notification/notification.module.d.ts +29 -19
  34. package/dist/notification/notification.module.js +338 -65
  35. package/dist/notification/notification.module.js.map +1 -1
  36. package/dist/notification/notification.service.d.ts +18 -2
  37. package/dist/notification/notification.service.js +277 -16
  38. package/dist/notification/notification.service.js.map +1 -1
  39. package/dist/notification/provider-config.d.ts +17 -0
  40. package/dist/{interface/payload.inteface.js → notification/provider-config.js} +1 -1
  41. package/dist/notification/provider-config.js.map +1 -0
  42. package/dist/push-notification/fcm-push-notification.provider.d.ts +3 -0
  43. package/dist/push-notification/fcm-push-notification.provider.js +17 -2
  44. package/dist/push-notification/fcm-push-notification.provider.js.map +1 -1
  45. package/dist/push-notification/firebase.interface.d.ts +1 -0
  46. package/dist/push-notification/index.d.ts +1 -4
  47. package/dist/push-notification/index.js +1 -5
  48. package/dist/push-notification/index.js.map +1 -1
  49. package/dist/push-notification/push-notification-provider.manager.d.ts +8 -3
  50. package/dist/push-notification/push-notification-provider.manager.js +12 -3
  51. package/dist/push-notification/push-notification-provider.manager.js.map +1 -1
  52. package/dist/push-notification/push-notification.provider.d.ts +1 -0
  53. package/dist/push-notification/push-notification.provider.js.map +1 -1
  54. package/dist/push-notification/push-notification.service.js +3 -2
  55. package/dist/push-notification/push-notification.service.js.map +1 -1
  56. package/dist/push-notification/user-token.store.js +7 -7
  57. package/dist/push-notification/user-token.store.js.map +1 -1
  58. package/dist/sms/constant.d.ts +1 -0
  59. package/dist/sms/constant.js +5 -0
  60. package/dist/sms/constant.js.map +1 -0
  61. package/dist/sms/index.d.ts +3 -2
  62. package/dist/sms/index.js +6 -2
  63. package/dist/sms/index.js.map +1 -1
  64. package/dist/sms/sms.provider.d.ts +9 -0
  65. package/dist/sms/sms.provider.js +7 -0
  66. package/dist/sms/sms.provider.js.map +1 -0
  67. package/dist/sms/sms.service.d.ts +8 -7
  68. package/dist/sms/sms.service.js +30 -2
  69. package/dist/sms/sms.service.js.map +1 -1
  70. package/dist/template/constant.d.ts +1 -0
  71. package/dist/template/constant.js +2 -1
  72. package/dist/template/constant.js.map +1 -1
  73. package/dist/template/engines/string-format.engine.d.ts +6 -0
  74. package/dist/template/engines/string-format.engine.js +17 -0
  75. package/dist/template/engines/string-format.engine.js.map +1 -0
  76. package/dist/template/fake-template.loader.js +14 -18
  77. package/dist/template/fake-template.loader.js.map +1 -1
  78. package/dist/template/index.d.ts +4 -2
  79. package/dist/template/index.js +4 -1
  80. package/dist/template/index.js.map +1 -1
  81. package/dist/template/template-engine.d.ts +5 -0
  82. package/dist/template/template-engine.js +3 -0
  83. package/dist/template/template-engine.js.map +1 -0
  84. package/dist/template/template.loader.d.ts +4 -1
  85. package/dist/template/template.loader.js +22 -14
  86. package/dist/template/template.loader.js.map +1 -1
  87. package/dist/whatsapp/constant.d.ts +2 -0
  88. package/dist/whatsapp/constant.js +6 -0
  89. package/dist/whatsapp/constant.js.map +1 -0
  90. package/dist/whatsapp/index.d.ts +3 -0
  91. package/dist/whatsapp/index.js +11 -0
  92. package/dist/whatsapp/index.js.map +1 -0
  93. package/dist/whatsapp/whatsapp.provider.d.ts +9 -0
  94. package/dist/whatsapp/whatsapp.provider.js +7 -0
  95. package/dist/whatsapp/whatsapp.provider.js.map +1 -0
  96. package/dist/whatsapp/whatsapp.service.d.ts +9 -0
  97. package/dist/whatsapp/whatsapp.service.js +35 -0
  98. package/dist/whatsapp/whatsapp.service.js.map +1 -0
  99. package/docs/20260214-fix-plan-notification.md +149 -0
  100. package/docs/20260214-fix-plan-post-refactor.md +118 -0
  101. package/docs/20260214-nestjs-notification-review.md +390 -0
  102. package/docs/20260214-review-post-refactor.md +285 -0
  103. package/docs/20260215-review-notification-post-fix.md +933 -0
  104. package/package.json +2 -1
  105. package/dist/email/email-provider.factory.d.ts +0 -5
  106. package/dist/email/email-provider.factory.js +0 -18
  107. package/dist/email/email-provider.factory.js.map +0 -1
  108. package/dist/email/email.module.d.ts +0 -27
  109. package/dist/email/email.module.js +0 -110
  110. package/dist/email/email.module.js.map +0 -1
  111. package/dist/interface/payload.inteface.d.ts +0 -22
  112. package/dist/interface/payload.inteface.js.map +0 -1
  113. package/dist/push-notification/push-notification-module.interface.d.ts +0 -33
  114. package/dist/push-notification/push-notification-module.interface.js.map +0 -1
  115. package/dist/push-notification/push-notification-provider-module.factory.d.ts +0 -5
  116. package/dist/push-notification/push-notification-provider-module.factory.js +0 -13
  117. package/dist/push-notification/push-notification-provider-module.factory.js.map +0 -1
  118. package/dist/push-notification/push-notification-provider.factory.d.ts +0 -5
  119. package/dist/push-notification/push-notification-provider.factory.js +0 -16
  120. package/dist/push-notification/push-notification-provider.factory.js.map +0 -1
  121. package/dist/push-notification/push-notification.module.d.ts +0 -15
  122. package/dist/push-notification/push-notification.module.js +0 -189
  123. package/dist/push-notification/push-notification.module.js.map +0 -1
package/CLAUDE.md ADDED
@@ -0,0 +1,21 @@
1
+ # @nathapp/nestjs-notification
2
+
3
+ Multi-channel notification library — Email, SMS, WhatsApp, Push.
4
+
5
+ ## Commands
6
+ - Test: `npx jest --config test/jest-test.json --no-coverage`
7
+ - Build: `npx tsc`
8
+
9
+ ## Provider/Service Pattern
10
+ - `XxxProvider` (abstract) → external devs extend this for custom implementations.
11
+ - `XxxService` (injectable wrapper) → internal, resolves provider via `XXX_PROVIDER` token.
12
+ - Push is special: uses `ProviderManager` for multi-provider (Android/iOS/Huawei).
13
+
14
+ ## Key Tokens
15
+ - `EMAIL_PROVIDER`, `SMS_PROVIDER`, `WHATSAPP_PROVIDER`, `PUSH_PROVIDER`
16
+ - Register via `NotificationModule.forRoot({ providers: [...] })`
17
+
18
+ ## Gotchas
19
+ - External providers extend `XxxProvider`, NOT `XxxService` (breaking change from v2).
20
+ - Template system is custom (not using Handlebars/EJS) — uses `{{variable}}` interpolation.
21
+ - Firebase push provider: ensure cleanup in `onModuleDestroy` to prevent memory leaks.
@@ -0,0 +1,307 @@
1
+ # @nathapp/nestjs-notification — Phase 7 & 8: WhatsApp Channel + Fallback
2
+
3
+ **Branch:** `feat/notification-enterprise-upgrade` (continue from Phase 6)
4
+ **Prerequisite commits:** Phases 1-6 complete, coverage at 81%+
5
+
6
+ ---
7
+
8
+ ## Phase 7: WhatsApp Channel Support
9
+
10
+ ### Problem
11
+ WhatsApp is a major messaging channel (especially in Asia) for OTPs, alerts, and transactional messages. It requires its own channel type because:
12
+ - Pre-approved template requirements (Meta Business API)
13
+ - Rich content support (buttons, media, lists)
14
+ - Session-based delivery model (24h reply window)
15
+ - Different from SMS — cannot reuse `SmsService`
16
+
17
+ ### Changes
18
+
19
+ #### 7.1 Create `WhatsAppMessage` interface
20
+ **File:** `lib/interface/message.interface.ts` (UPDATE)
21
+
22
+ ```typescript
23
+ export interface WhatsAppMessage extends BaseMessage {
24
+ /** WhatsApp template name (required by Meta Business API) */
25
+ templateName?: string;
26
+ /** Template language code (e.g., 'en', 'zh_CN') */
27
+ languageCode?: string;
28
+ /** Template header parameters */
29
+ headerParams?: string[];
30
+ /** Template body parameters */
31
+ bodyParams?: string[];
32
+ /** Optional media attachment URL */
33
+ mediaUrl?: string;
34
+ }
35
+ ```
36
+
37
+ #### 7.2 Create `WhatsAppService` abstract class
38
+ **File:** `lib/whatsapp/whatsapp.service.ts` (NEW)
39
+
40
+ ```typescript
41
+ import { WhatsAppMessage, SendResult } from '../interface';
42
+
43
+ export interface IWhatsAppService {
44
+ send(message: WhatsAppMessage): Promise<SendResult>;
45
+ }
46
+
47
+ export abstract class WhatsAppService implements IWhatsAppService {
48
+ abstract send(message: WhatsAppMessage): Promise<SendResult>;
49
+ }
50
+ ```
51
+
52
+ #### 7.3 Create WhatsApp index and constants
53
+ **File:** `lib/whatsapp/index.ts` (NEW)
54
+ **File:** `lib/whatsapp/constant.ts` (NEW) — `export const WHATSAPP_SERVICE = 'WHATSAPP_SERVICE';`
55
+
56
+ #### 7.4 Add `WhatsAppNotificationPayload` interface
57
+ **File:** `lib/interface/payload.inteface.ts` (UPDATE)
58
+
59
+ ```typescript
60
+ export interface WhatsAppNotificationPayload {
61
+ phoneNumber: string;
62
+ templateCode: string;
63
+ options?: {
64
+ locale?: string;
65
+ fallbackLocale?: string;
66
+ args?: any;
67
+ };
68
+ /** WhatsApp-specific: Meta template name (if different from templateCode) */
69
+ whatsappTemplateName?: string;
70
+ /** WhatsApp-specific: media attachment URL */
71
+ mediaUrl?: string;
72
+ }
73
+ ```
74
+
75
+ #### 7.5 Update `NotificationChannel` type
76
+ **File:** `lib/interface/payload.inteface.ts` (UPDATE)
77
+
78
+ ```typescript
79
+ export type NotificationChannel = 'email' | 'push' | 'sms' | 'whatsapp';
80
+ ```
81
+
82
+ #### 7.6 Add WhatsApp to `NotificationService`
83
+ **File:** `lib/notification/notification.service.ts` (UPDATE)
84
+
85
+ - Add `@Optional() @Inject(WhatsAppService) private readonly whatsAppService: WhatsAppService` to constructor
86
+ - Implement `sendWhatsApp(data: WhatsAppNotificationPayload): Promise<SendResult>` method
87
+ - Resolves template via `templateLoader`
88
+ - Calls `whatsAppService.send()` with resolved message
89
+ - Respects `strict` mode (throw if not configured vs return `{ success: false }`)
90
+ - Update `send()` unified method to handle `'whatsapp'` channel
91
+
92
+ #### 7.7 Add WhatsApp options to `NotificationModule`
93
+ **File:** `lib/notification/notification.module.ts` (UPDATE)
94
+
95
+ ```typescript
96
+ export interface NotificationModuleOptions {
97
+ // ... existing
98
+ whatsAppService?: Type<WhatsAppService>;
99
+ }
100
+ ```
101
+
102
+ - Register `WhatsAppService` provider when `whatsAppService` option is provided
103
+ - Support in both `forRoot` and `forRootAsync`
104
+
105
+ #### 7.8 Update `NotificationContext` for interceptors
106
+ **File:** `lib/notification/notification.interceptor.ts` (UPDATE)
107
+
108
+ The existing `NotificationChannel` type update will automatically make interceptors aware of `'whatsapp'`.
109
+
110
+ #### 7.9 Export new types
111
+ **File:** `lib/whatsapp/index.ts` (NEW)
112
+ **File:** `lib/index.ts` (UPDATE) — export WhatsApp types
113
+
114
+ ### Unit Tests
115
+
116
+ **File:** `test/notification/notification-whatsapp.service.spec.ts` (NEW)
117
+ - `sendWhatsApp()` with configured service → resolves template, calls `whatsAppService.send()`
118
+ - `sendWhatsApp()` without service + `strict: true` → throws `NotificationConfigError`
119
+ - `sendWhatsApp()` without service + `strict: false` → returns `{ success: false }`
120
+ - `send({ channels: ['whatsapp'] })` routes to `sendWhatsApp()`
121
+ - `send({ channels: ['email', 'whatsapp'] })` dispatches to both
122
+ - `sendWhatsApp()` passes `whatsappTemplateName` and `mediaUrl` to service
123
+
124
+ ### E2E Tests
125
+
126
+ **File:** `test/module/notification-whatsapp.module.e2e.spec.ts` (NEW)
127
+ - Register module with mock `WhatsAppService` → `sendWhatsApp()` works
128
+ - Register module without WhatsApp + `strict: false` → `send({ channels: ['whatsapp'] })` returns `{ success: false }`
129
+ - Full module with email + WhatsApp → unified `send()` dispatches to both
130
+
131
+ ---
132
+
133
+ ## Phase 8: Channel Fallback Support
134
+
135
+ ### Problem
136
+ When a preferred channel fails (e.g., WhatsApp undeliverable), the system should automatically try alternative channels. This must be:
137
+ - Configurable per-notification (not global)
138
+ - Observable via interceptors (log fallback events)
139
+ - Provider-agnostic (WhatsApp via Meta, SMS via Twilio — doesn't matter)
140
+
141
+ ### Changes
142
+
143
+ #### 8.1 Add `fallback` to `NotificationPayload`
144
+ **File:** `lib/interface/payload.inteface.ts` (UPDATE)
145
+
146
+ ```typescript
147
+ export interface NotificationPayload {
148
+ templateCode: string;
149
+ channels: NotificationChannel[];
150
+ /** Ordered fallback channels. Tried sequentially if ALL primary channels fail. */
151
+ fallback?: NotificationChannel[];
152
+ options?: {
153
+ locale?: string;
154
+ fallbackLocale?: string;
155
+ args?: any;
156
+ };
157
+ // Channel-specific recipient data
158
+ email?: string;
159
+ userId?: string | number;
160
+ deviceToken?: DeviceToken;
161
+ phoneNumber?: string;
162
+ /** WhatsApp-specific fields */
163
+ whatsappTemplateName?: string;
164
+ mediaUrl?: string;
165
+ // Optional
166
+ attachments?: EmailAttachment[];
167
+ }
168
+ ```
169
+
170
+ #### 8.2 Add `FallbackResult` to `NotificationResult`
171
+ **File:** `lib/interface/payload.inteface.ts` (UPDATE)
172
+
173
+ ```typescript
174
+ export interface NotificationResult {
175
+ results: Partial<Record<NotificationChannel, SendResult>>;
176
+ /** True if primary OR fallback succeeded */
177
+ success: boolean;
178
+ /** Which fallback channels were attempted (empty if primary succeeded) */
179
+ fallbackAttempted?: NotificationChannel[];
180
+ /** Which fallback channel succeeded (undefined if none) */
181
+ fallbackSucceeded?: NotificationChannel;
182
+ }
183
+ ```
184
+
185
+ #### 8.3 Update `send()` in `NotificationService` with fallback logic
186
+ **File:** `lib/notification/notification.service.ts` (UPDATE)
187
+
188
+ ```typescript
189
+ async send(payload: NotificationPayload): Promise<NotificationResult> {
190
+ // 1. Try all primary channels
191
+ const results: Partial<Record<NotificationChannel, SendResult>> = {};
192
+ for (const channel of payload.channels) {
193
+ results[channel] = await this.sendToChannel(channel, payload);
194
+ }
195
+
196
+ const primarySuccess = Object.values(results).some(r => r.success);
197
+
198
+ // 2. If ALL primary channels failed AND fallback is defined, try fallbacks
199
+ if (!primarySuccess && payload.fallback?.length) {
200
+ const fallbackAttempted: NotificationChannel[] = [];
201
+ let fallbackSucceeded: NotificationChannel | undefined;
202
+
203
+ for (const fallbackChannel of payload.fallback) {
204
+ fallbackAttempted.push(fallbackChannel);
205
+ const fallbackResult = await this.sendToChannel(fallbackChannel, payload);
206
+ results[fallbackChannel] = fallbackResult;
207
+
208
+ if (fallbackResult.success) {
209
+ fallbackSucceeded = fallbackChannel;
210
+ break; // Stop on first successful fallback
211
+ }
212
+ }
213
+
214
+ return {
215
+ results,
216
+ success: !!fallbackSucceeded,
217
+ fallbackAttempted,
218
+ fallbackSucceeded,
219
+ };
220
+ }
221
+
222
+ return {
223
+ results,
224
+ success: primarySuccess,
225
+ };
226
+ }
227
+
228
+ /** Internal: dispatch to a single channel */
229
+ private async sendToChannel(
230
+ channel: NotificationChannel,
231
+ payload: NotificationPayload,
232
+ ): Promise<SendResult> {
233
+ switch (channel) {
234
+ case 'email':
235
+ return this.sendEmail({ ... });
236
+ case 'push':
237
+ return this.sendPushNotification({ ... });
238
+ case 'sms':
239
+ return this.sendSms({ ... });
240
+ case 'whatsapp':
241
+ return this.sendWhatsApp({ ... });
242
+ default:
243
+ return { success: false, message: `Unknown channel: ${channel}` };
244
+ }
245
+ }
246
+ ```
247
+
248
+ #### 8.4 Update interceptors to include fallback context
249
+ **File:** `lib/notification/notification.interceptor.ts` (UPDATE)
250
+
251
+ ```typescript
252
+ export interface NotificationContext {
253
+ channel: NotificationChannel;
254
+ templateCode: string;
255
+ recipient: string;
256
+ resolvedMessage?: { subject?: string; content: string };
257
+ /** True if this send is a fallback attempt */
258
+ isFallback?: boolean;
259
+ /** The original channel that failed (when isFallback is true) */
260
+ failedChannel?: NotificationChannel;
261
+ }
262
+ ```
263
+
264
+ #### 8.5 Handle unconfigured fallback channels
265
+ **File:** `lib/notification/notification.service.ts` (UPDATE)
266
+
267
+ When a fallback channel's service is not configured:
268
+ - **`strict: true`** → throw `NotificationConfigError` (developer mistake — they specified a fallback they didn't configure)
269
+ - **`strict: false`** → skip that fallback channel silently (log a warning), move to next fallback in the list. If all fallbacks are unconfigured/failed, return `{ success: false }`.
270
+
271
+ The `sendToChannel()` method already handles this via the existing strict/lenient pattern. In lenient mode, an unconfigured channel returns `{ success: false }`, which the fallback loop treats as a failure and moves to the next channel.
272
+
273
+ ### Unit Tests
274
+
275
+ **File:** `test/notification/notification-fallback.spec.ts` (NEW)
276
+ - `send()` with `fallback: ['sms']` — primary email succeeds → no fallback attempted
277
+ - `send()` with `channels: ['whatsapp'], fallback: ['sms']` — WhatsApp fails → SMS attempted and succeeds
278
+ - `send()` with `channels: ['whatsapp'], fallback: ['sms']` — both fail → `success: false`, both in `fallbackAttempted`
279
+ - `send()` with `channels: ['whatsapp'], fallback: ['sms', 'email']` — WhatsApp fails, SMS fails, email succeeds → `fallbackSucceeded: 'email'`
280
+ - `send()` with no `fallback` — primary fails → no fallback, `success: false`
281
+ - `send()` with `channels: ['email', 'push']` — email fails, push succeeds → `success: true`, no fallback needed
282
+ - Interceptor `beforeSend` receives `isFallback: true` and `failedChannel` during fallback
283
+ - Interceptor `afterSend` receives fallback context
284
+ - **`send()` with `fallback: ['sms']` but SMS not configured + `strict: true` → throws `NotificationConfigError`**
285
+ - **`send()` with `fallback: ['sms']` but SMS not configured + `strict: false` → skips SMS, returns `{ success: false }`**
286
+ - **`send()` with `fallback: ['sms', 'email']` — SMS not configured (strict: false), email configured and succeeds → `fallbackSucceeded: 'email'`**
287
+
288
+ ### E2E Tests
289
+
290
+ **File:** `test/module/notification-fallback.module.e2e.spec.ts` (NEW)
291
+ - Full module with WhatsApp (mock fail) + SMS (mock success) → fallback works end-to-end
292
+ - Full module with email only, `fallback: ['email']` but email succeeds on primary → no fallback
293
+ - Verify interceptor hooks fire with correct `isFallback` flag
294
+
295
+ ---
296
+
297
+ ## Implementation Order
298
+
299
+ 1. **Phase 7** first (WhatsApp channel) — new channel, additive, no breaking changes
300
+ 2. **Phase 8** second (Fallback) — builds on Phase 7's `whatsapp` channel for the primary use case
301
+
302
+ ## Breaking Changes
303
+
304
+ **None.** Both phases are purely additive:
305
+ - `whatsapp` is a new channel option
306
+ - `fallback` is an optional field on `NotificationPayload`
307
+ - Existing `send()` calls without `fallback` behave identically
@@ -0,0 +1,361 @@
1
+ # @nathapp/nestjs-notification — Phase 9: Unified Provider Registry
2
+
3
+ **Branch:** `feat/notification-enterprise-upgrade` (continue from Phase 8)
4
+ **Breaking Change:** Yes — replaces `emailOptions` / `pushNotificationOptions` with unified `providers` array.
5
+
6
+ ---
7
+
8
+ ## Overview
9
+
10
+ Replace the per-channel module registration pattern (`EmailModule.forRoot()`, `PushNotificationModule.register()`) with a single, unified `providers` array on `NotificationModule`. Providers are just classes — no wrapper modules needed.
11
+
12
+ ### Before (current)
13
+ ```typescript
14
+ NotificationModule.forRoot({
15
+ emailOptions: EmailModule.forRoot({ option: { type: 'smtp', options: {...} } }),
16
+ pushNotificationOptions: PushNotificationModule.register({ providers: [...] }),
17
+ smsService: TwilioSmsProvider,
18
+ whatsAppService: TwilioWhatsAppProvider,
19
+ templateLoader: MyLoader,
20
+ })
21
+ ```
22
+
23
+ ### After (new)
24
+ ```typescript
25
+ NotificationModule.forRoot({
26
+ providers: [
27
+ { channel: 'email', provider: SmtpEmailProvider, options: { url: 'smtp://...', from: 'noreply@app.com' } },
28
+ { channel: 'push', provider: FcmPushNotificationProvider, options: { projectId: '...' } },
29
+ { channel: 'sms', provider: TwilioSmsProvider, options: { accountSid: '...', authToken: '...', from: '+1...' } },
30
+ { channel: 'whatsapp', provider: TwilioWhatsAppProvider, options: { accountSid: '...' } },
31
+ ],
32
+ templateLoader: MyLoader,
33
+ strict: true,
34
+ })
35
+ ```
36
+
37
+ ### Async with ModuleOptionsFactory
38
+ ```typescript
39
+ @Injectable()
40
+ export class AppNotificationOptionsFactory implements NotificationModuleOptionsFactory {
41
+ constructor(private config: ConfigService) {}
42
+
43
+ createOptions(): NotificationModuleOptions {
44
+ return {
45
+ providers: [
46
+ { channel: 'email', provider: SmtpEmailProvider, options: { url: this.config.get('SMTP_URL'), from: this.config.get('EMAIL_FROM') } },
47
+ ],
48
+ templateLoader: MyLoader,
49
+ };
50
+ }
51
+ }
52
+
53
+ NotificationModule.forRootAsync({
54
+ imports: [ConfigModule],
55
+ useClass: AppNotificationOptionsFactory,
56
+ })
57
+ ```
58
+
59
+ ---
60
+
61
+ ## Changes
62
+
63
+ ### 9.1 Create `ProviderConfig` interface
64
+ **File:** `lib/notification/provider-config.ts` (NEW)
65
+
66
+ ```typescript
67
+ import { Type } from '@nestjs/common';
68
+ import { NotificationChannel } from '../interface';
69
+ import { EmailProvider } from '../email';
70
+ import { PushNotificationProvider } from '../push-notification';
71
+ import { SmsService } from '../sms';
72
+ import { WhatsAppService } from '../whatsapp';
73
+
74
+ export type ChannelProviderMap = {
75
+ email: EmailProvider;
76
+ push: PushNotificationProvider;
77
+ sms: SmsService;
78
+ whatsapp: WhatsAppService;
79
+ };
80
+
81
+ export interface ProviderConfig<C extends NotificationChannel = NotificationChannel> {
82
+ channel: C;
83
+ provider: Type<ChannelProviderMap[C]>;
84
+ options?: any;
85
+ }
86
+ ```
87
+
88
+ ### 9.2 Create `NotificationProviderFactory`
89
+ **File:** `lib/notification/notification-provider.factory.ts` (NEW)
90
+
91
+ ```typescript
92
+ import { Logger } from '@nestjs/common';
93
+ import { ProviderConfig, ChannelProviderMap } from './provider-config';
94
+ import { EmailProvider } from '../email';
95
+ import { PushNotificationProvider } from '../push-notification';
96
+ import { SmsService } from '../sms';
97
+ import { WhatsAppService } from '../whatsapp';
98
+
99
+ const CHANNEL_BASE_CLASSES: Record<string, Function> = {
100
+ email: EmailProvider,
101
+ push: PushNotificationProvider,
102
+ sms: SmsService,
103
+ whatsapp: WhatsAppService,
104
+ };
105
+
106
+ export class NotificationProviderFactory {
107
+ private static readonly logger = new Logger(NotificationProviderFactory.name);
108
+
109
+ /**
110
+ * Create a provider instance from config.
111
+ * Validates that the provider class matches the expected base class for its channel.
112
+ * @throws Error if provider doesn't match its channel
113
+ */
114
+ static create<C extends keyof ChannelProviderMap>(
115
+ config: ProviderConfig<C>,
116
+ ): ChannelProviderMap[C] {
117
+ const { channel, provider, options } = config;
118
+ const baseClass = CHANNEL_BASE_CLASSES[channel];
119
+
120
+ if (!baseClass) {
121
+ throw new Error(`Unknown notification channel: ${channel}`);
122
+ }
123
+
124
+ // Validate: check prototype chain
125
+ const instance = new provider(options);
126
+
127
+ if (!(instance instanceof baseClass)) {
128
+ throw new Error(
129
+ `${provider.name} is not a valid ${channel} provider. ` +
130
+ `Expected ${baseClass.name} subclass.`,
131
+ );
132
+ }
133
+
134
+ this.logger.log(`Created ${channel} provider: ${provider.name}`);
135
+ return instance as ChannelProviderMap[C];
136
+ }
137
+ }
138
+ ```
139
+
140
+ ### 9.3 Rewrite `NotificationModule` options interfaces
141
+ **File:** `lib/notification/notification.module.ts` (REWRITE)
142
+
143
+ Replace the old `NotificationModuleOptions` and `NotificationModuleAsyncOptions` with:
144
+
145
+ ```typescript
146
+ export interface NotificationModuleOptions {
147
+ /** Unified provider registration */
148
+ providers?: ProviderConfig[];
149
+ /** Template loader class */
150
+ templateLoader: Type<TemplateLoader> | ProviderAsyncOptions<TemplateLoader>;
151
+ /** Template engine class (default: StringFormatEngine) */
152
+ templateEngine?: Type<TemplateEngine>;
153
+ /** Template service override */
154
+ templateService?: Type<TemplateService>;
155
+ /** Notification service override */
156
+ notificationService?: Type<NotificationService>;
157
+ /** Interceptors */
158
+ interceptors?: Type<NotificationInterceptor>[];
159
+ /** Strict mode (default: true) */
160
+ strict?: boolean;
161
+ /** Push notification provider manager override */
162
+ providerManager?: Type<ProviderManager>;
163
+ /** Push notification user token store override */
164
+ userTokenStore?: Type<UserTokenStore>;
165
+ }
166
+
167
+ export interface NotificationModuleAsyncOptions extends Pick<ModuleMetadata, 'imports'> {
168
+ /** Use a factory class to create options */
169
+ useClass?: Type<NotificationModuleOptionsFactory>;
170
+ /** Reuse an existing factory */
171
+ useExisting?: Type<NotificationModuleOptionsFactory>;
172
+ /** Inline factory function */
173
+ useFactory?: (...args: any[]) => NotificationModuleOptions | Promise<NotificationModuleOptions>;
174
+ inject?: any[];
175
+ }
176
+
177
+ export interface NotificationModuleOptionsFactory {
178
+ createOptions(): NotificationModuleOptions | Promise<NotificationModuleOptions>;
179
+ }
180
+ ```
181
+
182
+ ### 9.4 Rewrite `NotificationModule.forRoot()`
183
+ **File:** `lib/notification/notification.module.ts` (REWRITE)
184
+
185
+ The new `forRoot` should:
186
+ 1. Iterate `options.providers` array
187
+ 2. Use `NotificationProviderFactory.create()` for each
188
+ 3. Group by channel type
189
+ 4. Register each as the appropriate injection token:
190
+ - `email` → `EMAIL_PROVIDER` token + create `EmailService` wrapping it
191
+ - `push` → `PUSH_NOTIFICATION_PROVIDERS` token + create `PushNotificationService`
192
+ - `sms` → `SmsService` token
193
+ - `whatsapp` → `WhatsAppService` token
194
+ 5. Register template engine, template loader, template service, notification service, interceptors (same as current)
195
+ 6. Store `strict` option in `NOTIFICATION_OPTIONS`
196
+
197
+ **Key implementation detail:** When a channel has no provider registered, do NOT create its service. `NotificationService` already handles missing services via `@Optional()` inject + strict mode.
198
+
199
+ ### 9.5 Rewrite `NotificationModule.forRootAsync()`
200
+ **File:** `lib/notification/notification.module.ts` (REWRITE)
201
+
202
+ Support all three async patterns:
203
+
204
+ **`useClass`:**
205
+ ```typescript
206
+ NotificationModule.forRootAsync({
207
+ imports: [ConfigModule],
208
+ useClass: AppNotificationOptionsFactory,
209
+ })
210
+ ```
211
+ - NestJS creates `AppNotificationOptionsFactory`, injects its dependencies
212
+ - Calls `factory.createOptions()` to get `NotificationModuleOptions`
213
+ - Then same provider registration as `forRoot`
214
+
215
+ **`useExisting`:**
216
+ ```typescript
217
+ NotificationModule.forRootAsync({
218
+ useExisting: AppNotificationOptionsFactory,
219
+ })
220
+ ```
221
+ - Reuses an already-registered factory
222
+
223
+ **`useFactory`:**
224
+ ```typescript
225
+ NotificationModule.forRootAsync({
226
+ imports: [ConfigModule],
227
+ useFactory: (config: ConfigService) => ({
228
+ providers: [
229
+ { channel: 'email', provider: SmtpEmailProvider, options: { url: config.get('SMTP_URL') } },
230
+ ],
231
+ templateLoader: MyLoader,
232
+ }),
233
+ inject: [ConfigService],
234
+ })
235
+ ```
236
+
237
+ ### 9.6 Keep `EmailModule` and `PushNotificationModule` as internal
238
+ **File:** `lib/email/email.module.ts`, `lib/push-notification/push-notification.module.ts`
239
+
240
+ Do NOT delete these modules. They still work internally. But they are no longer the primary public API. The public API is `NotificationModule.forRoot({ providers: [...] })`.
241
+
242
+ Remove `EmailModule` and `PushNotificationModule` from the main `index.ts` exports. Keep them accessible for advanced use cases via deep imports, but they're no longer the recommended approach.
243
+
244
+ ### 9.7 Update `NotificationService` constructor
245
+ **File:** `lib/notification/notification.service.ts` (UPDATE)
246
+
247
+ The constructor already uses `@Optional()` for all services. No changes needed here — the module registration changes handle which services are available.
248
+
249
+ ### 9.8 Export new types
250
+ **File:** `lib/notification/index.ts` (UPDATE)
251
+ **File:** `lib/index.ts` (UPDATE)
252
+
253
+ Export: `ProviderConfig`, `ChannelProviderMap`, `NotificationProviderFactory`, `NotificationModuleOptionsFactory`
254
+
255
+ Remove from top-level exports: `EmailModule`, `EmailModuleOptions`, `EmailModuleAsyncOptions`, `PushNotificationModule`, `PushNotificationModuleOptions`, `PushNotificationModuleAsyncOptions`
256
+
257
+ ---
258
+
259
+ ## Unit Tests
260
+
261
+ **File:** `test/notification/notification-provider.factory.spec.ts` (NEW)
262
+ - `create()` with `SmtpEmailProvider` + channel `email` → returns instance
263
+ - `create()` with `FcmPushNotificationProvider` + channel `push` → returns instance
264
+ - `create()` with options → passes options to constructor
265
+ - `create()` with mismatched provider/channel → throws descriptive error (e.g., `TwilioSmsProvider` on `email` channel)
266
+ - `create()` with unknown channel → throws `Unknown notification channel` error
267
+ - `create()` without options → creates provider with undefined options (no crash)
268
+
269
+ **File:** `test/notification/notification-module-unified.spec.ts` (NEW)
270
+ - `forRoot()` with single email provider → `EmailService` is available, `NotificationService` can send email
271
+ - `forRoot()` with multiple channels → all services registered
272
+ - `forRoot()` with no providers → no services available, strict mode throws on send
273
+ - `forRoot()` with `strict: false` + no providers → `send()` returns `{ success: false }`
274
+ - `forRoot()` with custom `templateEngine` → engine is used
275
+ - `forRoot()` with `interceptors` → interceptors are called
276
+
277
+ **File:** `test/notification/notification-module-async.spec.ts` (NEW)
278
+ - `forRootAsync()` with `useFactory` → providers created from factory result
279
+ - `forRootAsync()` with `useClass` (NotificationModuleOptionsFactory) → factory `createOptions()` called, providers registered
280
+ - `forRootAsync()` with `useExisting` → reuses existing factory
281
+ - `forRootAsync()` with `inject: [ConfigService]` → config values passed to factory
282
+
283
+ ## E2E Tests
284
+
285
+ **File:** `test/module/notification-unified-providers.module.e2e.spec.ts` (NEW)
286
+ - Full module with `providers: [{ channel: 'email', provider: SmtpEmailProvider, options: {...} }]` → email sending works
287
+ - Full module with email + sms providers → both channels work via unified `send()`
288
+ - Module with mismatched provider → bootstrap throws
289
+ - `forRootAsync` with `useClass` factory → full e2e send works
290
+
291
+ **File:** `test/module/notification-options-factory.module.e2e.spec.ts` (NEW)
292
+ - `useClass: TestOptionsFactory` → factory is instantiated, `createOptions()` called
293
+ - Factory receives injected dependencies (e.g., a mock config service)
294
+ - Options from factory are used to create providers correctly
295
+
296
+ ---
297
+
298
+ ## Migration Guide (for consumers)
299
+
300
+ ### Before
301
+ ```typescript
302
+ NotificationModule.forRoot({
303
+ emailOptions: { option: { type: 'smtp', options: { url: '...', from: '...' } } },
304
+ pushNotificationOptions: { providers: [{ name: 'fcm', ... }] },
305
+ templateLoader: MyLoader,
306
+ })
307
+ ```
308
+
309
+ ### After
310
+ ```typescript
311
+ NotificationModule.forRoot({
312
+ providers: [
313
+ { channel: 'email', provider: SmtpEmailProvider, options: { url: '...', from: '...' } },
314
+ { channel: 'push', provider: FcmPushNotificationProvider, options: { projectId: '...' } },
315
+ ],
316
+ templateLoader: MyLoader,
317
+ })
318
+ ```
319
+
320
+ ### Async Before
321
+ ```typescript
322
+ NotificationModule.forRootAsync({
323
+ emailOptions: {
324
+ useFactory: (config) => ({ option: { type: 'smtp', options: { url: config.get('SMTP') } } }),
325
+ inject: [ConfigService],
326
+ },
327
+ templateLoader: MyLoader,
328
+ })
329
+ ```
330
+
331
+ ### Async After
332
+ ```typescript
333
+ NotificationModule.forRootAsync({
334
+ imports: [ConfigModule],
335
+ useClass: AppNotificationOptionsFactory,
336
+ })
337
+ // or
338
+ NotificationModule.forRootAsync({
339
+ imports: [ConfigModule],
340
+ useFactory: (config: ConfigService) => ({
341
+ providers: [
342
+ { channel: 'email', provider: SmtpEmailProvider, options: { url: config.get('SMTP') } },
343
+ ],
344
+ templateLoader: MyLoader,
345
+ }),
346
+ inject: [ConfigService],
347
+ })
348
+ ```
349
+
350
+ ---
351
+
352
+ ## Breaking Changes
353
+
354
+ 1. `NotificationModuleOptions.emailOptions` → removed (use `providers` array)
355
+ 2. `NotificationModuleOptions.pushNotificationOptions` → removed (use `providers` array)
356
+ 3. `NotificationModuleOptions.smsService` → removed (use `providers` array)
357
+ 4. `NotificationModuleOptions.whatsAppService` → removed (use `providers` array)
358
+ 5. `EmailModule`, `PushNotificationModule` no longer exported from main index (still accessible via deep import)
359
+ 6. `EmailModuleOptions`, `PushNotificationModuleOptions` etc. no longer exported from main index
360
+
361
+ All changes are within the **same v2.2.0 release** since no prior release has shipped these new features yet.